@agentunion/kite 1.2.0 → 1.3.0
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.
- package/CHANGELOG.md +208 -0
- package/README.md +48 -0
- package/cli.js +1 -1
- package/extensions/agents/assistant/entry.py +30 -81
- package/extensions/agents/assistant/module.md +1 -1
- package/extensions/agents/assistant/server.py +83 -122
- package/extensions/channels/acp_channel/entry.py +30 -81
- package/extensions/channels/acp_channel/module.md +1 -1
- package/extensions/channels/acp_channel/server.py +83 -122
- package/extensions/event_hub_bench/entry.py +81 -121
- package/extensions/services/backup/entry.py +213 -85
- package/extensions/services/model_service/entry.py +213 -85
- package/extensions/services/watchdog/entry.py +513 -460
- package/extensions/services/watchdog/monitor.py +55 -69
- package/extensions/services/web/entry.py +11 -108
- package/extensions/services/web/server.py +120 -77
- package/{core/registry → kernel}/entry.py +65 -37
- package/{core/event_hub/hub.py → kernel/event_hub.py} +61 -81
- package/kernel/module.md +33 -0
- package/{core/registry/store.py → kernel/registry_store.py} +13 -4
- package/kernel/rpc_router.py +388 -0
- package/kernel/server.py +267 -0
- package/launcher/__init__.py +10 -0
- package/launcher/__main__.py +6 -0
- package/launcher/count_lines.py +258 -0
- package/{core/launcher → launcher}/entry.py +693 -767
- package/launcher/logging_setup.py +289 -0
- package/{core/launcher → launcher}/module_scanner.py +11 -6
- package/main.py +11 -350
- package/package.json +6 -9
- package/__init__.py +0 -1
- package/__main__.py +0 -15
- package/core/event_hub/BENCHMARK.md +0 -94
- package/core/event_hub/__init__.py +0 -0
- package/core/event_hub/bench.py +0 -459
- package/core/event_hub/bench_extreme.py +0 -308
- package/core/event_hub/bench_perf.py +0 -350
- package/core/event_hub/entry.py +0 -436
- package/core/event_hub/module.md +0 -20
- package/core/event_hub/server.py +0 -269
- package/core/kite_log.py +0 -241
- package/core/launcher/__init__.py +0 -0
- package/core/registry/__init__.py +0 -0
- package/core/registry/module.md +0 -30
- package/core/registry/server.py +0 -339
- package/extensions/services/backup/server.py +0 -244
- package/extensions/services/model_service/server.py +0 -236
- package/extensions/services/watchdog/server.py +0 -229
- /package/{core → kernel}/__init__.py +0 -0
- /package/{core/event_hub → kernel}/dedup.py +0 -0
- /package/{core/event_hub → kernel}/router.py +0 -0
- /package/{core/launcher → launcher}/module.md +0 -0
- /package/{core/launcher → launcher}/process_manager.py +0 -0
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
"""
|
|
2
|
-
ACP Channel
|
|
3
|
-
|
|
4
|
-
Connects to Event Hub via WebSocket for event publishing and subscription.
|
|
5
|
-
Sends periodic heartbeat to Registry and test events to Event Hub.
|
|
2
|
+
ACP Channel WebSocket client.
|
|
3
|
+
Connects to Kernel via WebSocket JSON-RPC 2.0 for event publishing and subscription.
|
|
6
4
|
"""
|
|
7
5
|
|
|
8
6
|
import asyncio
|
|
@@ -11,127 +9,101 @@ import time
|
|
|
11
9
|
import uuid
|
|
12
10
|
from datetime import datetime, timezone
|
|
13
11
|
|
|
14
|
-
import httpx
|
|
15
12
|
import websockets
|
|
16
|
-
from fastapi import FastAPI
|
|
17
13
|
|
|
18
14
|
|
|
19
15
|
class AcpChannelServer:
|
|
20
16
|
|
|
21
|
-
def __init__(self, token: str = "",
|
|
22
|
-
event_hub_ws: str = "", boot_t0: float = 0):
|
|
17
|
+
def __init__(self, token: str = "", kernel_port: int = 0, boot_t0: float = 0):
|
|
23
18
|
self.token = token
|
|
24
|
-
self.
|
|
25
|
-
self.event_hub_ws = event_hub_ws
|
|
19
|
+
self.kernel_port = kernel_port
|
|
26
20
|
self.boot_t0 = boot_t0
|
|
27
21
|
self._ws_task: asyncio.Task | None = None
|
|
28
|
-
self._heartbeat_task: asyncio.Task | None = None
|
|
29
22
|
self._test_task: asyncio.Task | None = None
|
|
30
23
|
self._ws: object | None = None
|
|
31
24
|
self._ready_sent = False
|
|
32
25
|
self._shutting_down = False
|
|
33
|
-
self._uvicorn_server = None # set by entry.py for graceful shutdown
|
|
34
26
|
self._start_time = time.time()
|
|
35
|
-
self.app = self._create_app()
|
|
36
|
-
|
|
37
|
-
def _create_app(self) -> FastAPI:
|
|
38
|
-
app = FastAPI(title="Kite ACP Channel", docs_url=None, redoc_url=None)
|
|
39
|
-
server = self
|
|
40
|
-
|
|
41
|
-
@app.on_event("startup")
|
|
42
|
-
async def _startup():
|
|
43
|
-
server._heartbeat_task = asyncio.create_task(server._heartbeat_loop())
|
|
44
|
-
if server.event_hub_ws:
|
|
45
|
-
server._ws_task = asyncio.create_task(server._ws_loop())
|
|
46
|
-
server._test_task = asyncio.create_task(server._test_event_loop())
|
|
47
|
-
|
|
48
|
-
@app.on_event("shutdown")
|
|
49
|
-
async def _shutdown():
|
|
50
|
-
if server._heartbeat_task:
|
|
51
|
-
server._heartbeat_task.cancel()
|
|
52
|
-
if server._ws_task:
|
|
53
|
-
server._ws_task.cancel()
|
|
54
|
-
if server._test_task:
|
|
55
|
-
server._test_task.cancel()
|
|
56
|
-
if server._ws:
|
|
57
|
-
await server._ws.close()
|
|
58
|
-
print("[acp_channel] Shutdown complete")
|
|
59
|
-
|
|
60
|
-
@app.get("/health")
|
|
61
|
-
async def health():
|
|
62
|
-
return {
|
|
63
|
-
"status": "healthy",
|
|
64
|
-
"details": {
|
|
65
|
-
"event_hub_connected": server._ws is not None,
|
|
66
|
-
"uptime_seconds": round(time.time() - server._start_time),
|
|
67
|
-
},
|
|
68
|
-
}
|
|
69
27
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
28
|
+
async def run(self):
|
|
29
|
+
"""Main entry point: start WebSocket loop and test event loop."""
|
|
30
|
+
if self.kernel_port:
|
|
31
|
+
self._ws_task = asyncio.create_task(self._ws_loop())
|
|
32
|
+
self._test_task = asyncio.create_task(self._test_event_loop())
|
|
33
|
+
|
|
34
|
+
# Wait for tasks to complete
|
|
35
|
+
tasks = [t for t in [self._ws_task, self._test_task] if t]
|
|
36
|
+
if tasks:
|
|
37
|
+
await asyncio.gather(*tasks, return_exceptions=True)
|
|
78
38
|
|
|
79
|
-
|
|
39
|
+
print("[acp_channel] Shutdown complete")
|
|
80
40
|
|
|
81
|
-
# ──
|
|
41
|
+
# ── Kernel WebSocket client ──
|
|
82
42
|
|
|
83
43
|
async def _ws_loop(self):
|
|
84
|
-
"""Connect to
|
|
85
|
-
retry_delay = 0.5
|
|
86
|
-
max_delay = 30
|
|
44
|
+
"""Connect to Kernel, subscribe, register, and listen. Reconnect on failure."""
|
|
45
|
+
retry_delay = 0.5
|
|
46
|
+
max_delay = 30
|
|
87
47
|
while not self._shutting_down:
|
|
88
48
|
try:
|
|
89
49
|
await self._ws_connect()
|
|
90
50
|
except asyncio.CancelledError:
|
|
91
51
|
return
|
|
92
|
-
retry_delay = 0.5 # reset on successful connection
|
|
93
52
|
except Exception as e:
|
|
94
|
-
print(f"[acp_channel]
|
|
53
|
+
print(f"[acp_channel] Kernel connection error: {e}, retrying in {retry_delay:.1f}s")
|
|
95
54
|
self._ws = None
|
|
96
55
|
if self._shutting_down:
|
|
97
56
|
return
|
|
98
57
|
await asyncio.sleep(retry_delay)
|
|
99
|
-
retry_delay = min(retry_delay * 2, max_delay)
|
|
58
|
+
retry_delay = min(retry_delay * 2, max_delay)
|
|
100
59
|
|
|
101
60
|
async def _ws_connect(self):
|
|
102
|
-
"""Single WebSocket session: connect, subscribe, receive loop."""
|
|
103
|
-
url = f"{self.
|
|
104
|
-
print(f"[acp_channel]
|
|
61
|
+
"""Single WebSocket session: connect, subscribe, register, ready, receive loop."""
|
|
62
|
+
url = f"ws://127.0.0.1:{self.kernel_port}/ws?token={self.token}&id=acp_channel"
|
|
63
|
+
print(f"[acp_channel] Connecting to Kernel (port {self.kernel_port})")
|
|
105
64
|
async with websockets.connect(url, open_timeout=3, ping_interval=None, ping_timeout=None, close_timeout=10) as ws:
|
|
106
65
|
self._ws = ws
|
|
107
66
|
elapsed = time.monotonic() - self.boot_t0 if self.boot_t0 else 0
|
|
108
67
|
elapsed_str = f" ({elapsed:.1f}s)" if elapsed else ""
|
|
109
|
-
print(f"[acp_channel] Connected to
|
|
68
|
+
print(f"[acp_channel] Connected to Kernel{elapsed_str}")
|
|
110
69
|
|
|
111
|
-
# Subscribe to
|
|
112
|
-
await
|
|
113
|
-
"type": "subscribe",
|
|
70
|
+
# Step 1: Subscribe to events (先订阅)
|
|
71
|
+
await self._rpc_call(ws, "event.subscribe", {
|
|
114
72
|
"events": ["module.started", "module.stopped", "module.shutdown"],
|
|
115
|
-
})
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
# Step 2: Register to Kernel (再注册)
|
|
76
|
+
await self._rpc_call(ws, "registry.register", {
|
|
77
|
+
"module_id": "acp_channel",
|
|
78
|
+
"module_type": "channel",
|
|
79
|
+
"name": "ACP Channel",
|
|
80
|
+
"events_publish": {
|
|
81
|
+
"acp_channel.test": {},
|
|
82
|
+
},
|
|
83
|
+
"events_subscribe": [
|
|
84
|
+
"module.started",
|
|
85
|
+
"module.stopped",
|
|
86
|
+
"module.shutdown",
|
|
87
|
+
],
|
|
88
|
+
})
|
|
116
89
|
|
|
117
|
-
#
|
|
90
|
+
# Step 3: Publish module.ready (once)
|
|
118
91
|
if not self._ready_sent:
|
|
119
|
-
|
|
120
|
-
"type": "event",
|
|
92
|
+
await self._rpc_call(ws, "event.publish", {
|
|
121
93
|
"event_id": str(uuid.uuid4()),
|
|
122
94
|
"event": "module.ready",
|
|
123
|
-
"source": "acp_channel",
|
|
124
|
-
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
125
95
|
"data": {
|
|
126
96
|
"module_id": "acp_channel",
|
|
127
97
|
"graceful_shutdown": True,
|
|
128
98
|
},
|
|
129
|
-
}
|
|
130
|
-
await ws.send(json.dumps(ready_msg))
|
|
99
|
+
})
|
|
131
100
|
self._ready_sent = True
|
|
132
101
|
elapsed = time.monotonic() - self.boot_t0 if self.boot_t0 else 0
|
|
133
102
|
elapsed_str = f" ({elapsed:.1f}s)" if elapsed else ""
|
|
134
|
-
print(f"[acp_channel] module.ready
|
|
103
|
+
print(f"[acp_channel] module.ready published{elapsed_str}")
|
|
104
|
+
|
|
105
|
+
# Reset retry delay on successful connection
|
|
106
|
+
retry_delay = 0.5
|
|
135
107
|
|
|
136
108
|
# Receive loop
|
|
137
109
|
async for raw in ws:
|
|
@@ -141,18 +113,22 @@ class AcpChannelServer:
|
|
|
141
113
|
continue
|
|
142
114
|
|
|
143
115
|
try:
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
116
|
+
has_method = "method" in msg
|
|
117
|
+
has_id = "id" in msg
|
|
118
|
+
|
|
119
|
+
if has_method and not has_id:
|
|
120
|
+
# JSON-RPC Notification (event delivery)
|
|
121
|
+
params = msg.get("params", {})
|
|
122
|
+
event_name = params.get("event", "")
|
|
147
123
|
if event_name == "module.shutdown":
|
|
148
|
-
|
|
124
|
+
data = params.get("data", {})
|
|
125
|
+
target = data.get("module_id", "")
|
|
149
126
|
if target == "acp_channel":
|
|
150
127
|
await self._handle_shutdown(ws)
|
|
151
128
|
return
|
|
152
|
-
elif
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
print(f"[acp_channel] Event Hub error: {msg.get('message')}")
|
|
129
|
+
elif not has_method and has_id:
|
|
130
|
+
# JSON-RPC Response (to our RPC calls)
|
|
131
|
+
pass
|
|
156
132
|
except Exception as e:
|
|
157
133
|
print(f"[acp_channel] 事件处理异常(已忽略): {e}")
|
|
158
134
|
|
|
@@ -162,64 +138,49 @@ class AcpChannelServer:
|
|
|
162
138
|
self._shutting_down = True
|
|
163
139
|
|
|
164
140
|
# Step 1: Send ack
|
|
165
|
-
await self.
|
|
141
|
+
await self._rpc_call(ws, "event.publish", {
|
|
142
|
+
"event_id": str(uuid.uuid4()),
|
|
166
143
|
"event": "module.shutdown.ack",
|
|
167
144
|
"data": {"module_id": "acp_channel", "estimated_cleanup": 2},
|
|
168
145
|
})
|
|
169
146
|
print("[acp_channel] shutdown ack sent")
|
|
170
147
|
|
|
171
148
|
# Step 2: Cleanup (cancel background tasks)
|
|
172
|
-
if self._heartbeat_task:
|
|
173
|
-
self._heartbeat_task.cancel()
|
|
174
149
|
if self._test_task:
|
|
175
150
|
self._test_task.cancel()
|
|
176
151
|
|
|
177
152
|
# Step 3: Send ready (before closing WS!)
|
|
178
|
-
await self.
|
|
153
|
+
await self._rpc_call(ws, "event.publish", {
|
|
154
|
+
"event_id": str(uuid.uuid4()),
|
|
179
155
|
"event": "module.shutdown.ready",
|
|
180
156
|
"data": {"module_id": "acp_channel"},
|
|
181
157
|
})
|
|
182
|
-
print("[acp_channel] Shutdown
|
|
158
|
+
print("[acp_channel] Shutdown ready sent")
|
|
183
159
|
|
|
184
|
-
# Step 4:
|
|
185
|
-
|
|
186
|
-
|
|
160
|
+
# Step 4: Exit process
|
|
161
|
+
import sys
|
|
162
|
+
sys.exit(0)
|
|
163
|
+
|
|
164
|
+
async def _rpc_call(self, ws, method: str, params: dict = None):
|
|
165
|
+
"""Send a JSON-RPC 2.0 request."""
|
|
166
|
+
msg = {"jsonrpc": "2.0", "id": str(uuid.uuid4()), "method": method}
|
|
167
|
+
if params:
|
|
168
|
+
msg["params"] = params
|
|
169
|
+
await ws.send(json.dumps(msg))
|
|
187
170
|
|
|
188
171
|
async def _publish_event(self, event: dict):
|
|
189
|
-
"""Publish an event
|
|
172
|
+
"""Publish an event via JSON-RPC event.publish."""
|
|
190
173
|
if not self._ws:
|
|
191
174
|
return
|
|
192
|
-
msg = {
|
|
193
|
-
"type": "event",
|
|
194
|
-
"event_id": str(uuid.uuid4()),
|
|
195
|
-
"event": event.get("event", ""),
|
|
196
|
-
"source": "acp_channel",
|
|
197
|
-
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
198
|
-
"data": event.get("data", {}),
|
|
199
|
-
}
|
|
200
175
|
try:
|
|
201
|
-
await self._ws.
|
|
176
|
+
await self._rpc_call(self._ws, "event.publish", {
|
|
177
|
+
"event_id": str(uuid.uuid4()),
|
|
178
|
+
"event": event.get("event", ""),
|
|
179
|
+
"data": event.get("data", {}),
|
|
180
|
+
})
|
|
202
181
|
except Exception as e:
|
|
203
182
|
print(f"[acp_channel] Failed to publish event: {e}")
|
|
204
183
|
|
|
205
|
-
# ── Heartbeat to Registry ──
|
|
206
|
-
|
|
207
|
-
async def _heartbeat_loop(self):
|
|
208
|
-
"""Send heartbeat to Registry every 30 seconds."""
|
|
209
|
-
while True:
|
|
210
|
-
await asyncio.sleep(30)
|
|
211
|
-
try:
|
|
212
|
-
async with httpx.AsyncClient() as client:
|
|
213
|
-
await client.post(
|
|
214
|
-
f"{self.registry_url}/modules",
|
|
215
|
-
json={"action": "heartbeat", "module_id": "acp_channel"},
|
|
216
|
-
headers={"Authorization": f"Bearer {self.token}"},
|
|
217
|
-
timeout=5,
|
|
218
|
-
)
|
|
219
|
-
print("[acp_channel] heartbeat sent")
|
|
220
|
-
except Exception:
|
|
221
|
-
pass
|
|
222
|
-
|
|
223
184
|
# ── Test event loop ──
|
|
224
185
|
|
|
225
186
|
async def _test_event_loop(self):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Event Hub Benchmark module entry point.
|
|
3
|
-
|
|
3
|
+
Connects to Kernel via WebSocket JSON-RPC 2.0, runs benchmarks, saves results, exits.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
import asyncio
|
|
@@ -231,54 +231,10 @@ if module_data:
|
|
|
231
231
|
_setup_exception_hooks()
|
|
232
232
|
|
|
233
233
|
|
|
234
|
-
def
|
|
235
|
-
"""
|
|
236
|
-
payload = {
|
|
237
|
-
"action": "register",
|
|
238
|
-
"module_id": "event_hub_bench",
|
|
239
|
-
"module_type": "tool",
|
|
240
|
-
"name": "Event Hub Benchmark",
|
|
241
|
-
}
|
|
242
|
-
headers = {"Authorization": f"Bearer {token}"}
|
|
243
|
-
try:
|
|
244
|
-
resp = client.post(
|
|
245
|
-
f"{registry_url}/modules",
|
|
246
|
-
json=payload, headers=headers,
|
|
247
|
-
)
|
|
248
|
-
if resp.status_code == 200:
|
|
249
|
-
print(f"{TAG} Registered to Registry")
|
|
250
|
-
else:
|
|
251
|
-
print(f"{TAG} WARNING: Registry returned {resp.status_code}")
|
|
252
|
-
except Exception as e:
|
|
253
|
-
print(f"{TAG} WARNING: Registry registration failed: {e}")
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
def _discover_hub(client: httpx.Client, registry_url: str, token: str, timeout: float = 30) -> str | None:
|
|
257
|
-
"""Poll registry until event_hub is found. Returns ws:// URL or None."""
|
|
258
|
-
headers = {"Authorization": f"Bearer {token}"}
|
|
259
|
-
deadline = time.time() + timeout
|
|
260
|
-
while time.time() < deadline:
|
|
261
|
-
try:
|
|
262
|
-
resp = client.get(
|
|
263
|
-
f"{registry_url}/get/event_hub",
|
|
264
|
-
headers=headers,
|
|
265
|
-
)
|
|
266
|
-
if resp.status_code == 200:
|
|
267
|
-
info = resp.json()
|
|
268
|
-
ws_url = (info.get("metadata") or {}).get("ws_endpoint")
|
|
269
|
-
if ws_url:
|
|
270
|
-
return ws_url
|
|
271
|
-
except Exception:
|
|
272
|
-
pass
|
|
273
|
-
time.sleep(0.2)
|
|
274
|
-
return None
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
def _get_hub_stats(ws_url: str) -> dict:
|
|
278
|
-
"""Fetch hub /stats via HTTP (derive from ws:// URL)."""
|
|
279
|
-
http_url = ws_url.replace("ws://", "http://").rsplit("/ws", 1)[0]
|
|
234
|
+
def _get_kernel_stats(kernel_port: int) -> dict:
|
|
235
|
+
"""Fetch Kernel /stats via HTTP."""
|
|
280
236
|
try:
|
|
281
|
-
resp = httpx.get(f"{
|
|
237
|
+
resp = httpx.get(f"http://127.0.0.1:{kernel_port}/stats", timeout=5)
|
|
282
238
|
if resp.status_code == 200:
|
|
283
239
|
return resp.json()
|
|
284
240
|
except Exception:
|
|
@@ -286,16 +242,14 @@ def _get_hub_stats(ws_url: str) -> dict:
|
|
|
286
242
|
return {}
|
|
287
243
|
|
|
288
244
|
|
|
289
|
-
def
|
|
290
|
-
"""Sample
|
|
245
|
+
def _sample_kernel_resources(kernel_port: int) -> dict:
|
|
246
|
+
"""Sample Kernel process CPU/memory via psutil. Returns {} if unavailable."""
|
|
291
247
|
try:
|
|
292
248
|
import psutil
|
|
293
249
|
except ImportError:
|
|
294
250
|
return {"error": "psutil not installed"}
|
|
295
|
-
http_url = ws_url.replace("ws://", "http://").rsplit("/ws", 1)[0]
|
|
296
|
-
port = int(http_url.rsplit(":", 1)[1].split("/")[0])
|
|
297
251
|
for conn in psutil.net_connections(kind="tcp"):
|
|
298
|
-
if conn.laddr.port ==
|
|
252
|
+
if conn.laddr.port == kernel_port and conn.status == "LISTEN":
|
|
299
253
|
try:
|
|
300
254
|
p = psutil.Process(conn.pid)
|
|
301
255
|
mem = p.memory_info()
|
|
@@ -308,13 +262,13 @@ def _sample_hub_resources(ws_url: str) -> dict:
|
|
|
308
262
|
}
|
|
309
263
|
except Exception:
|
|
310
264
|
pass
|
|
311
|
-
return {"error": "
|
|
265
|
+
return {"error": "kernel process not found"}
|
|
312
266
|
|
|
313
267
|
|
|
314
268
|
# ── WebSocket client ──
|
|
315
269
|
|
|
316
270
|
class WsClient:
|
|
317
|
-
"""Lightweight async WebSocket client for
|
|
271
|
+
"""Lightweight async WebSocket client for Kernel benchmarks (JSON-RPC 2.0)."""
|
|
318
272
|
|
|
319
273
|
def __init__(self, ws_url: str, token: str, client_id: str):
|
|
320
274
|
self.url = f"{ws_url}?token={token}&id={client_id}"
|
|
@@ -325,16 +279,18 @@ class WsClient:
|
|
|
325
279
|
self.ws = await websockets.connect(self.url, max_size=None)
|
|
326
280
|
|
|
327
281
|
async def subscribe(self, patterns: list[str]):
|
|
328
|
-
|
|
282
|
+
msg = {"jsonrpc": "2.0", "id": str(uuid.uuid4()), "method": "event.subscribe", "params": {"events": patterns}}
|
|
283
|
+
await self.ws.send(json.dumps(msg))
|
|
329
284
|
|
|
330
285
|
async def publish(self, event_type: str, data: dict) -> str:
|
|
331
286
|
eid = str(uuid.uuid4())
|
|
332
|
-
|
|
333
|
-
"
|
|
334
|
-
"
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
}
|
|
287
|
+
msg = {
|
|
288
|
+
"jsonrpc": "2.0", "id": str(uuid.uuid4()), "method": "event.publish",
|
|
289
|
+
"params": {
|
|
290
|
+
"event_id": eid, "event": event_type, "data": data,
|
|
291
|
+
},
|
|
292
|
+
}
|
|
293
|
+
await self.ws.send(json.dumps(msg))
|
|
338
294
|
return eid
|
|
339
295
|
|
|
340
296
|
async def recv(self, timeout=5.0):
|
|
@@ -351,9 +307,10 @@ class WsClient:
|
|
|
351
307
|
# ── Benchmark runner ──
|
|
352
308
|
|
|
353
309
|
class BenchRunner:
|
|
354
|
-
def __init__(self, ws_url: str, token: str):
|
|
310
|
+
def __init__(self, ws_url: str, token: str, kernel_port: int):
|
|
355
311
|
self.ws_url = ws_url
|
|
356
312
|
self.token = token
|
|
313
|
+
self.kernel_port = kernel_port
|
|
357
314
|
self.results = {}
|
|
358
315
|
|
|
359
316
|
async def _make_client(self, name: str) -> WsClient:
|
|
@@ -378,7 +335,9 @@ class BenchRunner:
|
|
|
378
335
|
while not stop.is_set():
|
|
379
336
|
try:
|
|
380
337
|
raw = await asyncio.wait_for(sub.ws.recv(), timeout=0.5)
|
|
381
|
-
|
|
338
|
+
msg = json.loads(raw)
|
|
339
|
+
# JSON-RPC Notification (event): has method, no id
|
|
340
|
+
if "method" in msg and "id" not in msg:
|
|
382
341
|
recvd += 1
|
|
383
342
|
except Exception:
|
|
384
343
|
pass
|
|
@@ -393,7 +352,7 @@ class BenchRunner:
|
|
|
393
352
|
recv_task = asyncio.create_task(_recv())
|
|
394
353
|
ack_task = asyncio.create_task(_drain_acks())
|
|
395
354
|
|
|
396
|
-
hub_before = (
|
|
355
|
+
hub_before = (_get_kernel_stats(self.kernel_port).get("counters") or {})
|
|
397
356
|
|
|
398
357
|
t0 = time.time()
|
|
399
358
|
for i in range(n):
|
|
@@ -409,7 +368,7 @@ class BenchRunner:
|
|
|
409
368
|
await recv_task
|
|
410
369
|
ack_task.cancel()
|
|
411
370
|
|
|
412
|
-
hub_after = (
|
|
371
|
+
hub_after = (_get_kernel_stats(self.kernel_port).get("counters") or {})
|
|
413
372
|
|
|
414
373
|
await pub.close()
|
|
415
374
|
await sub.close()
|
|
@@ -440,9 +399,9 @@ class BenchRunner:
|
|
|
440
399
|
for i in range(n):
|
|
441
400
|
t0 = time.time()
|
|
442
401
|
await pub.publish("bench.lat.test", {"i": i})
|
|
443
|
-
await pub.recv(timeout=2) #
|
|
402
|
+
await pub.recv(timeout=2) # RPC response
|
|
444
403
|
msg = await sub.recv(timeout=2)
|
|
445
|
-
if msg and
|
|
404
|
+
if msg and "method" in msg and "id" not in msg:
|
|
446
405
|
latencies.append((time.time() - t0) * 1000)
|
|
447
406
|
|
|
448
407
|
await pub.close()
|
|
@@ -481,7 +440,8 @@ class BenchRunner:
|
|
|
481
440
|
while not stop.is_set():
|
|
482
441
|
try:
|
|
483
442
|
raw = await asyncio.wait_for(client.ws.recv(), timeout=0.5)
|
|
484
|
-
|
|
443
|
+
msg = json.loads(raw)
|
|
444
|
+
if "method" in msg and "id" not in msg:
|
|
485
445
|
counts[idx] += 1
|
|
486
446
|
except Exception:
|
|
487
447
|
pass
|
|
@@ -527,13 +487,13 @@ class BenchRunner:
|
|
|
527
487
|
|
|
528
488
|
# ── Save results ──
|
|
529
489
|
|
|
530
|
-
def save(self
|
|
490
|
+
def save(self):
|
|
531
491
|
os.makedirs(RESULTS_DIR, exist_ok=True)
|
|
532
492
|
now = datetime.now()
|
|
533
493
|
filepath = os.path.join(RESULTS_DIR, now.strftime("%Y-%m-%d_%H-%M-%S") + ".json")
|
|
534
494
|
|
|
535
|
-
|
|
536
|
-
resources =
|
|
495
|
+
kernel_stats = _get_kernel_stats(self.kernel_port)
|
|
496
|
+
resources = _sample_kernel_resources(self.kernel_port)
|
|
537
497
|
|
|
538
498
|
data = {
|
|
539
499
|
"timestamp": now.isoformat(),
|
|
@@ -542,7 +502,7 @@ class BenchRunner:
|
|
|
542
502
|
"python": platform.python_version(),
|
|
543
503
|
},
|
|
544
504
|
"hub_resources": resources,
|
|
545
|
-
"hub_counters":
|
|
505
|
+
"hub_counters": kernel_stats.get("counters", {}),
|
|
546
506
|
**self.results,
|
|
547
507
|
}
|
|
548
508
|
with open(filepath, "w", encoding="utf-8") as f:
|
|
@@ -551,45 +511,60 @@ class BenchRunner:
|
|
|
551
511
|
|
|
552
512
|
|
|
553
513
|
def _send_ready_event(ws_url: str, token: str):
|
|
554
|
-
"""Send module.ready to
|
|
514
|
+
"""Send module.ready to Kernel via JSON-RPC 2.0. Startup phase complete."""
|
|
555
515
|
try:
|
|
556
516
|
import websockets.sync.client as ws_sync
|
|
557
517
|
url = f"{ws_url}?token={token}&id=event_hub_bench"
|
|
558
518
|
with ws_sync.connect(url, close_timeout=3) as ws:
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
"
|
|
562
|
-
"
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
519
|
+
# Subscribe (先订阅)
|
|
520
|
+
ws.send(json.dumps({
|
|
521
|
+
"jsonrpc": "2.0", "id": str(uuid.uuid4()),
|
|
522
|
+
"method": "event.subscribe", "params": {"events": []},
|
|
523
|
+
}))
|
|
524
|
+
# Register (再注册)
|
|
525
|
+
ws.send(json.dumps({
|
|
526
|
+
"jsonrpc": "2.0", "id": str(uuid.uuid4()),
|
|
527
|
+
"method": "registry.register",
|
|
528
|
+
"params": {
|
|
529
|
+
"module_id": "event_hub_bench",
|
|
530
|
+
"module_type": "tool",
|
|
531
|
+
"name": "Event Hub Benchmark",
|
|
532
|
+
},
|
|
533
|
+
}))
|
|
534
|
+
# Publish ready
|
|
535
|
+
ws.send(json.dumps({
|
|
536
|
+
"jsonrpc": "2.0", "id": str(uuid.uuid4()),
|
|
537
|
+
"method": "event.publish",
|
|
538
|
+
"params": {
|
|
539
|
+
"event_id": str(uuid.uuid4()),
|
|
540
|
+
"event": "module.ready",
|
|
541
|
+
"data": {"module_id": "event_hub_bench"},
|
|
542
|
+
},
|
|
543
|
+
}))
|
|
568
544
|
time.sleep(0.1)
|
|
569
545
|
except Exception as e:
|
|
570
546
|
print(f"{TAG} WARNING: Could not send module.ready: {e}")
|
|
571
547
|
|
|
572
548
|
|
|
573
549
|
def _send_exiting_event(ws_url: str, token: str, reason: str):
|
|
574
|
-
"""Send module.exiting event to
|
|
550
|
+
"""Send module.exiting event to Kernel via JSON-RPC 2.0 before exit."""
|
|
575
551
|
try:
|
|
576
552
|
import websockets.sync.client as ws_sync
|
|
577
553
|
url = f"{ws_url}?token={token}&id=event_hub_bench"
|
|
578
554
|
with ws_sync.connect(url, close_timeout=3) as ws:
|
|
579
|
-
|
|
580
|
-
"
|
|
581
|
-
"
|
|
582
|
-
"
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
555
|
+
ws.send(json.dumps({
|
|
556
|
+
"jsonrpc": "2.0", "id": str(uuid.uuid4()),
|
|
557
|
+
"method": "event.publish",
|
|
558
|
+
"params": {
|
|
559
|
+
"event_id": str(uuid.uuid4()),
|
|
560
|
+
"event": "module.exiting",
|
|
561
|
+
"data": {
|
|
562
|
+
"module_id": "event_hub_bench",
|
|
563
|
+
"reason": reason,
|
|
564
|
+
"action": "none",
|
|
565
|
+
},
|
|
589
566
|
},
|
|
590
|
-
}
|
|
591
|
-
ws.send(json.dumps(msg))
|
|
592
|
-
# Brief wait for delivery
|
|
567
|
+
}))
|
|
593
568
|
time.sleep(0.3)
|
|
594
569
|
except Exception as e:
|
|
595
570
|
print(f"{TAG} WARNING: Could not send module.exiting: {e}")
|
|
@@ -597,12 +572,12 @@ def _send_exiting_event(ws_url: str, token: str, reason: str):
|
|
|
597
572
|
|
|
598
573
|
# ── Entry point ──
|
|
599
574
|
|
|
600
|
-
async def _run(ws_url: str, token: str):
|
|
601
|
-
bench = BenchRunner(ws_url, token)
|
|
575
|
+
async def _run(ws_url: str, token: str, kernel_port: int):
|
|
576
|
+
bench = BenchRunner(ws_url, token, kernel_port)
|
|
602
577
|
await bench.bench_throughput()
|
|
603
578
|
await bench.bench_latency()
|
|
604
579
|
await bench.bench_fanout()
|
|
605
|
-
bench.save(
|
|
580
|
+
bench.save()
|
|
606
581
|
|
|
607
582
|
|
|
608
583
|
def main():
|
|
@@ -617,29 +592,14 @@ def main():
|
|
|
617
592
|
pass
|
|
618
593
|
|
|
619
594
|
# Read registry_port from environment variable
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
if not token or not registry_port:
|
|
623
|
-
print(f"{TAG} ERROR: Missing token or KITE_REGISTRY_PORT")
|
|
624
|
-
sys.exit(1)
|
|
625
|
-
|
|
626
|
-
registry_url = f"http://127.0.0.1:{registry_port}"
|
|
627
|
-
|
|
628
|
-
client = httpx.Client(timeout=5)
|
|
629
|
-
|
|
630
|
-
# Register to Registry first (identity declaration)
|
|
631
|
-
_register_to_registry(client, registry_url, token)
|
|
632
|
-
|
|
633
|
-
print(f"{TAG} Discovering event_hub via registry...")
|
|
634
|
-
|
|
635
|
-
ws_url = _discover_hub(client, registry_url, token)
|
|
595
|
+
kernel_port = int(os.environ.get("KITE_KERNEL_PORT", "0"))
|
|
636
596
|
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
print(f"{TAG} ERROR: event_hub not found")
|
|
597
|
+
if not token or not kernel_port:
|
|
598
|
+
print(f"{TAG} ERROR: Missing token or KITE_KERNEL_PORT")
|
|
640
599
|
sys.exit(1)
|
|
641
600
|
|
|
642
|
-
|
|
601
|
+
ws_url = f"ws://127.0.0.1:{kernel_port}/ws"
|
|
602
|
+
print(f"{TAG} Kernel at port {kernel_port}")
|
|
643
603
|
|
|
644
604
|
# Debug mode required: multiple benchmark clients share one token
|
|
645
605
|
if os.environ.get("KITE_DEBUG") != "1":
|
|
@@ -653,7 +613,7 @@ def main():
|
|
|
653
613
|
# ── Business logic: run benchmarks ──
|
|
654
614
|
print(f"{TAG} Starting benchmarks...")
|
|
655
615
|
|
|
656
|
-
asyncio.run(_run(ws_url, token))
|
|
616
|
+
asyncio.run(_run(ws_url, token, kernel_port))
|
|
657
617
|
|
|
658
618
|
print(f"{TAG} Benchmarks complete, sending exit intent...")
|
|
659
619
|
_send_exiting_event(ws_url, token, "benchmarks complete")
|