@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,236 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Model Service HTTP server.
|
|
3
|
-
Exposes /health and /status endpoints.
|
|
4
|
-
Connects to Event Hub via WebSocket for event publishing and subscription.
|
|
5
|
-
Sends periodic heartbeat to Registry and test events to Event Hub.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
import asyncio
|
|
9
|
-
import json
|
|
10
|
-
import time
|
|
11
|
-
import uuid
|
|
12
|
-
from datetime import datetime, timezone
|
|
13
|
-
|
|
14
|
-
import httpx
|
|
15
|
-
import websockets
|
|
16
|
-
from fastapi import FastAPI
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class ModelServiceServer:
|
|
20
|
-
|
|
21
|
-
def __init__(self, token: str = "", registry_url: str = "",
|
|
22
|
-
event_hub_ws: str = "", boot_t0: float = 0):
|
|
23
|
-
self.token = token
|
|
24
|
-
self.registry_url = registry_url
|
|
25
|
-
self.event_hub_ws = event_hub_ws
|
|
26
|
-
self.boot_t0 = boot_t0
|
|
27
|
-
self._ws_task: asyncio.Task | None = None
|
|
28
|
-
self._heartbeat_task: asyncio.Task | None = None
|
|
29
|
-
self._test_task: asyncio.Task | None = None
|
|
30
|
-
self._ws: object | None = None
|
|
31
|
-
self._ready_sent = False
|
|
32
|
-
self._shutting_down = False
|
|
33
|
-
self._uvicorn_server = None # set by entry.py for graceful shutdown
|
|
34
|
-
self._start_time = time.time()
|
|
35
|
-
self.app = self._create_app()
|
|
36
|
-
|
|
37
|
-
def _create_app(self) -> FastAPI:
|
|
38
|
-
app = FastAPI(title="Kite Model Service", 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("[model_service] 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
|
-
|
|
70
|
-
@app.get("/status")
|
|
71
|
-
async def status():
|
|
72
|
-
return {
|
|
73
|
-
"module": "model_service",
|
|
74
|
-
"status": "running",
|
|
75
|
-
"event_hub_connected": server._ws is not None,
|
|
76
|
-
"uptime_seconds": round(time.time() - server._start_time),
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
return app
|
|
80
|
-
|
|
81
|
-
# ── Event Hub WebSocket client ──
|
|
82
|
-
|
|
83
|
-
async def _ws_loop(self):
|
|
84
|
-
"""Connect to Event Hub, subscribe, and listen. Reconnect on failure."""
|
|
85
|
-
retry_delay = 0.5 # start with 0.5s
|
|
86
|
-
max_delay = 30 # cap at 30s
|
|
87
|
-
while not self._shutting_down:
|
|
88
|
-
try:
|
|
89
|
-
await self._ws_connect()
|
|
90
|
-
except asyncio.CancelledError:
|
|
91
|
-
return
|
|
92
|
-
retry_delay = 0.5 # reset on successful connection
|
|
93
|
-
except Exception as e:
|
|
94
|
-
print(f"[model_service] Event Hub connection error: {e}, retrying in {retry_delay:.1f}s")
|
|
95
|
-
self._ws = None
|
|
96
|
-
if self._shutting_down:
|
|
97
|
-
return
|
|
98
|
-
await asyncio.sleep(retry_delay)
|
|
99
|
-
retry_delay = min(retry_delay * 2, max_delay) # exponential backoff
|
|
100
|
-
|
|
101
|
-
async def _ws_connect(self):
|
|
102
|
-
"""Single WebSocket session: connect, subscribe, receive loop."""
|
|
103
|
-
url = f"{self.event_hub_ws}?token={self.token}&id=model_service"
|
|
104
|
-
print(f"[model_service] WS connecting to {self.event_hub_ws}")
|
|
105
|
-
async with websockets.connect(url, open_timeout=3, ping_interval=None, ping_timeout=None, close_timeout=10) as ws:
|
|
106
|
-
self._ws = ws
|
|
107
|
-
elapsed = time.monotonic() - self.boot_t0 if self.boot_t0 else 0
|
|
108
|
-
elapsed_str = f" ({elapsed:.1f}s)" if elapsed else ""
|
|
109
|
-
print(f"[model_service] Connected to Event Hub{elapsed_str}")
|
|
110
|
-
|
|
111
|
-
# Subscribe to module lifecycle events + shutdown
|
|
112
|
-
await ws.send(json.dumps({
|
|
113
|
-
"type": "subscribe",
|
|
114
|
-
"events": ["module.started", "module.stopped", "module.shutdown"],
|
|
115
|
-
}))
|
|
116
|
-
|
|
117
|
-
# Send module.ready (once) so Launcher knows we're up
|
|
118
|
-
if not self._ready_sent:
|
|
119
|
-
ready_msg = {
|
|
120
|
-
"type": "event",
|
|
121
|
-
"event_id": str(uuid.uuid4()),
|
|
122
|
-
"event": "module.ready",
|
|
123
|
-
"source": "model_service",
|
|
124
|
-
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
125
|
-
"data": {
|
|
126
|
-
"module_id": "model_service",
|
|
127
|
-
"graceful_shutdown": True,
|
|
128
|
-
},
|
|
129
|
-
}
|
|
130
|
-
await ws.send(json.dumps(ready_msg))
|
|
131
|
-
self._ready_sent = True
|
|
132
|
-
elapsed = time.monotonic() - self.boot_t0 if self.boot_t0 else 0
|
|
133
|
-
elapsed_str = f" ({elapsed:.1f}s)" if elapsed else ""
|
|
134
|
-
print(f"[model_service] module.ready sent{elapsed_str}")
|
|
135
|
-
|
|
136
|
-
# Receive loop
|
|
137
|
-
async for raw in ws:
|
|
138
|
-
try:
|
|
139
|
-
msg = json.loads(raw)
|
|
140
|
-
except (json.JSONDecodeError, TypeError):
|
|
141
|
-
continue
|
|
142
|
-
|
|
143
|
-
try:
|
|
144
|
-
msg_type = msg.get("type", "")
|
|
145
|
-
if msg_type == "event":
|
|
146
|
-
event_name = msg.get("event", "")
|
|
147
|
-
if event_name == "module.shutdown":
|
|
148
|
-
target = (msg.get("data") if isinstance(msg.get("data"), dict) else {}).get("module_id", "")
|
|
149
|
-
if target == "model_service":
|
|
150
|
-
await self._handle_shutdown(ws)
|
|
151
|
-
return
|
|
152
|
-
elif msg_type == "ack":
|
|
153
|
-
pass # publish confirmed
|
|
154
|
-
elif msg_type == "error":
|
|
155
|
-
print(f"[model_service] Event Hub error: {msg.get('message')}")
|
|
156
|
-
except Exception as e:
|
|
157
|
-
print(f"[model_service] 事件处理异常(已忽略): {e}")
|
|
158
|
-
|
|
159
|
-
async def _handle_shutdown(self, ws):
|
|
160
|
-
"""Handle module.shutdown: ack → cleanup → ready → exit."""
|
|
161
|
-
print("[model_service] Received module.shutdown")
|
|
162
|
-
self._shutting_down = True
|
|
163
|
-
|
|
164
|
-
# Step 1: Send ack
|
|
165
|
-
await self._publish_event({
|
|
166
|
-
"event": "module.shutdown.ack",
|
|
167
|
-
"data": {"module_id": "model_service", "estimated_cleanup": 2},
|
|
168
|
-
})
|
|
169
|
-
print("[model_service] shutdown ack sent")
|
|
170
|
-
|
|
171
|
-
# Step 2: Cleanup (cancel background tasks)
|
|
172
|
-
if self._heartbeat_task:
|
|
173
|
-
self._heartbeat_task.cancel()
|
|
174
|
-
if self._test_task:
|
|
175
|
-
self._test_task.cancel()
|
|
176
|
-
|
|
177
|
-
# Step 3: Send ready (before closing WS!)
|
|
178
|
-
await self._publish_event({
|
|
179
|
-
"event": "module.shutdown.ready",
|
|
180
|
-
"data": {"module_id": "model_service"},
|
|
181
|
-
})
|
|
182
|
-
print("[model_service] Shutdown complete")
|
|
183
|
-
|
|
184
|
-
# Step 4: Trigger uvicorn exit (WS will close when uvicorn shuts down)
|
|
185
|
-
if self._uvicorn_server:
|
|
186
|
-
self._uvicorn_server.should_exit = True
|
|
187
|
-
|
|
188
|
-
async def _publish_event(self, event: dict):
|
|
189
|
-
"""Publish an event to Event Hub via WebSocket."""
|
|
190
|
-
if not self._ws:
|
|
191
|
-
return
|
|
192
|
-
msg = {
|
|
193
|
-
"type": "event",
|
|
194
|
-
"event_id": str(uuid.uuid4()),
|
|
195
|
-
"event": event.get("event", ""),
|
|
196
|
-
"source": "model_service",
|
|
197
|
-
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
198
|
-
"data": event.get("data", {}),
|
|
199
|
-
}
|
|
200
|
-
try:
|
|
201
|
-
await self._ws.send(json.dumps(msg))
|
|
202
|
-
except Exception as e:
|
|
203
|
-
print(f"[model_service] Failed to publish event: {e}")
|
|
204
|
-
|
|
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": "model_service"},
|
|
216
|
-
headers={"Authorization": f"Bearer {self.token}"},
|
|
217
|
-
timeout=5,
|
|
218
|
-
)
|
|
219
|
-
print("[model_service] heartbeat sent")
|
|
220
|
-
except Exception:
|
|
221
|
-
pass
|
|
222
|
-
|
|
223
|
-
# ── Test event loop ──
|
|
224
|
-
|
|
225
|
-
async def _test_event_loop(self):
|
|
226
|
-
"""Publish a test event every 10 seconds."""
|
|
227
|
-
while True:
|
|
228
|
-
await asyncio.sleep(10)
|
|
229
|
-
await self._publish_event({
|
|
230
|
-
"event": "model_service.test",
|
|
231
|
-
"data": {
|
|
232
|
-
"message": "test event from model_service",
|
|
233
|
-
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
234
|
-
},
|
|
235
|
-
})
|
|
236
|
-
print("[model_service] test event published")
|
|
@@ -1,229 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Watchdog HTTP server.
|
|
3
|
-
Exposes /health and /status endpoints. Runs the monitor loop on startup.
|
|
4
|
-
Connects to Event Hub via WebSocket for event publishing and subscription.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
import asyncio
|
|
8
|
-
import json
|
|
9
|
-
import time
|
|
10
|
-
import uuid
|
|
11
|
-
|
|
12
|
-
import httpx
|
|
13
|
-
import websockets
|
|
14
|
-
from fastapi import FastAPI
|
|
15
|
-
|
|
16
|
-
from .monitor import HealthMonitor
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class WatchdogServer:
|
|
20
|
-
|
|
21
|
-
def __init__(self, monitor: HealthMonitor, token: str = "",
|
|
22
|
-
event_hub_ws: str = ""):
|
|
23
|
-
self.monitor = monitor
|
|
24
|
-
self.token = token
|
|
25
|
-
self.event_hub_ws = event_hub_ws
|
|
26
|
-
self._monitor_task: asyncio.Task | None = None
|
|
27
|
-
self._ws_task: asyncio.Task | None = None
|
|
28
|
-
self._heartbeat_task: asyncio.Task | None = None
|
|
29
|
-
self._ws: object | None = None
|
|
30
|
-
self._ready_sent = False
|
|
31
|
-
self._start_time = time.time()
|
|
32
|
-
self._uvicorn_server = None # set by entry.py for graceful shutdown
|
|
33
|
-
self._shutting_down = False
|
|
34
|
-
self.app = self._create_app()
|
|
35
|
-
|
|
36
|
-
# Wire up publish callback on monitor
|
|
37
|
-
self.monitor.publish_event = self._publish_event
|
|
38
|
-
|
|
39
|
-
def _create_app(self) -> FastAPI:
|
|
40
|
-
app = FastAPI(title="Kite Watchdog", docs_url=None, redoc_url=None)
|
|
41
|
-
server = self
|
|
42
|
-
|
|
43
|
-
@app.on_event("startup")
|
|
44
|
-
async def _startup():
|
|
45
|
-
server._monitor_task = asyncio.create_task(server.monitor.run())
|
|
46
|
-
server._heartbeat_task = asyncio.create_task(server._heartbeat_loop())
|
|
47
|
-
if server.event_hub_ws:
|
|
48
|
-
server._ws_task = asyncio.create_task(server._ws_loop())
|
|
49
|
-
server._test_task = asyncio.create_task(server._test_event_loop())
|
|
50
|
-
|
|
51
|
-
@app.on_event("shutdown")
|
|
52
|
-
async def _shutdown():
|
|
53
|
-
server.monitor.stop()
|
|
54
|
-
if server._monitor_task:
|
|
55
|
-
server._monitor_task.cancel()
|
|
56
|
-
if server._heartbeat_task:
|
|
57
|
-
server._heartbeat_task.cancel()
|
|
58
|
-
if server._ws_task:
|
|
59
|
-
server._ws_task.cancel()
|
|
60
|
-
if hasattr(server, '_test_task') and server._test_task:
|
|
61
|
-
server._test_task.cancel()
|
|
62
|
-
if server._ws:
|
|
63
|
-
await server._ws.close()
|
|
64
|
-
|
|
65
|
-
@app.get("/health")
|
|
66
|
-
async def health():
|
|
67
|
-
return {
|
|
68
|
-
"status": "healthy",
|
|
69
|
-
"details": {
|
|
70
|
-
"monitored_modules": len(server.monitor.modules),
|
|
71
|
-
"event_hub_connected": server._ws is not None,
|
|
72
|
-
"uptime_seconds": round(time.time() - server._start_time),
|
|
73
|
-
},
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
@app.get("/status")
|
|
77
|
-
async def status():
|
|
78
|
-
return server.monitor.get_status()
|
|
79
|
-
|
|
80
|
-
return app
|
|
81
|
-
|
|
82
|
-
# ── Event Hub WebSocket client ──
|
|
83
|
-
|
|
84
|
-
async def _ws_loop(self):
|
|
85
|
-
"""Connect to Event Hub, subscribe, and listen. Reconnect on failure."""
|
|
86
|
-
retry_delay = 0.5 # start with 0.5s
|
|
87
|
-
max_delay = 30 # cap at 30s
|
|
88
|
-
while not self._shutting_down:
|
|
89
|
-
try:
|
|
90
|
-
await self._ws_connect()
|
|
91
|
-
retry_delay = 0.5 # reset on successful connection
|
|
92
|
-
except asyncio.CancelledError:
|
|
93
|
-
return
|
|
94
|
-
except Exception as e:
|
|
95
|
-
print(f"[watchdog] Event Hub connection error: {e}, retrying in {retry_delay:.1f}s")
|
|
96
|
-
self._ws = None
|
|
97
|
-
if self._shutting_down:
|
|
98
|
-
return
|
|
99
|
-
await asyncio.sleep(retry_delay)
|
|
100
|
-
retry_delay = min(retry_delay * 2, max_delay) # exponential backoff
|
|
101
|
-
|
|
102
|
-
async def _ws_connect(self):
|
|
103
|
-
"""Single WebSocket session: connect, subscribe, receive loop."""
|
|
104
|
-
url = f"{self.event_hub_ws}?token={self.token}&id=watchdog"
|
|
105
|
-
print(f"[watchdog] WS connecting to {self.event_hub_ws}")
|
|
106
|
-
async with websockets.connect(url, open_timeout=3, ping_interval=None, ping_timeout=None, close_timeout=10) as ws:
|
|
107
|
-
self._ws = ws
|
|
108
|
-
print("[watchdog] Connected to Event Hub")
|
|
109
|
-
|
|
110
|
-
# Subscribe to module lifecycle events
|
|
111
|
-
await ws.send(json.dumps({
|
|
112
|
-
"type": "subscribe",
|
|
113
|
-
"events": ["system.ready", "module.started", "module.stopped", "module.exiting", "module.ready", "module.shutdown"],
|
|
114
|
-
}))
|
|
115
|
-
|
|
116
|
-
# Send module.ready (once) so Launcher knows we're up
|
|
117
|
-
if not self._ready_sent:
|
|
118
|
-
from datetime import datetime, timezone
|
|
119
|
-
ready_msg = {
|
|
120
|
-
"type": "event",
|
|
121
|
-
"event_id": str(uuid.uuid4()),
|
|
122
|
-
"event": "module.ready",
|
|
123
|
-
"source": "watchdog",
|
|
124
|
-
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
125
|
-
"data": {
|
|
126
|
-
"module_id": "watchdog",
|
|
127
|
-
"graceful_shutdown": True,
|
|
128
|
-
},
|
|
129
|
-
}
|
|
130
|
-
await ws.send(json.dumps(ready_msg))
|
|
131
|
-
self._ready_sent = True
|
|
132
|
-
|
|
133
|
-
# Receive loop
|
|
134
|
-
async for raw in ws:
|
|
135
|
-
try:
|
|
136
|
-
msg = json.loads(raw)
|
|
137
|
-
except (json.JSONDecodeError, TypeError):
|
|
138
|
-
continue
|
|
139
|
-
|
|
140
|
-
try:
|
|
141
|
-
msg_type = msg.get("type", "")
|
|
142
|
-
if msg_type == "event":
|
|
143
|
-
event = msg.get("event", "")
|
|
144
|
-
data = msg.get("data") if isinstance(msg.get("data"), dict) else {}
|
|
145
|
-
if event == "module.shutdown" and data.get("module_id") == "watchdog":
|
|
146
|
-
await self._handle_shutdown(data)
|
|
147
|
-
return
|
|
148
|
-
await self.monitor.handle_event(msg)
|
|
149
|
-
elif msg_type == "ack":
|
|
150
|
-
pass # publish confirmed
|
|
151
|
-
elif msg_type == "error":
|
|
152
|
-
print(f"[watchdog] Event Hub error: {msg.get('message')}")
|
|
153
|
-
except Exception as e:
|
|
154
|
-
print(f"[watchdog] 事件处理异常(已忽略): {e}")
|
|
155
|
-
|
|
156
|
-
async def _handle_shutdown(self, data: dict):
|
|
157
|
-
"""Handle module.shutdown event — ack, cleanup, ready, exit."""
|
|
158
|
-
print("[watchdog] Received shutdown request")
|
|
159
|
-
self._shutting_down = True
|
|
160
|
-
# Step 1: Send ack
|
|
161
|
-
await self._publish_event({
|
|
162
|
-
"event": "module.shutdown.ack",
|
|
163
|
-
"data": {"module_id": "watchdog", "estimated_cleanup": 2},
|
|
164
|
-
})
|
|
165
|
-
# Step 2: Cleanup
|
|
166
|
-
self.monitor.stop()
|
|
167
|
-
if self._monitor_task:
|
|
168
|
-
self._monitor_task.cancel()
|
|
169
|
-
if self._heartbeat_task:
|
|
170
|
-
self._heartbeat_task.cancel()
|
|
171
|
-
if hasattr(self, '_test_task') and self._test_task:
|
|
172
|
-
self._test_task.cancel()
|
|
173
|
-
# Step 3: Send ready (before closing WS!)
|
|
174
|
-
await self._publish_event({
|
|
175
|
-
"event": "module.shutdown.ready",
|
|
176
|
-
"data": {"module_id": "watchdog"},
|
|
177
|
-
})
|
|
178
|
-
print("[watchdog] Shutdown ready, exiting")
|
|
179
|
-
# Step 4: Trigger uvicorn exit (WS will close when uvicorn shuts down)
|
|
180
|
-
if self._uvicorn_server:
|
|
181
|
-
self._uvicorn_server.should_exit = True
|
|
182
|
-
|
|
183
|
-
async def _publish_event(self, event: dict):
|
|
184
|
-
"""Publish an event to Event Hub via WebSocket."""
|
|
185
|
-
if not self._ws:
|
|
186
|
-
return
|
|
187
|
-
from datetime import datetime, timezone
|
|
188
|
-
msg = {
|
|
189
|
-
"type": "event",
|
|
190
|
-
"event_id": str(uuid.uuid4()),
|
|
191
|
-
"event": event.get("event", ""),
|
|
192
|
-
"source": "watchdog",
|
|
193
|
-
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
194
|
-
"data": event.get("data", {}),
|
|
195
|
-
}
|
|
196
|
-
try:
|
|
197
|
-
await self._ws.send(json.dumps(msg))
|
|
198
|
-
except Exception as e:
|
|
199
|
-
print(f"[watchdog] Failed to publish event: {e}")
|
|
200
|
-
|
|
201
|
-
# ── Heartbeat to Registry ──
|
|
202
|
-
|
|
203
|
-
async def _heartbeat_loop(self):
|
|
204
|
-
"""Send heartbeat to Registry every 30 seconds."""
|
|
205
|
-
while True:
|
|
206
|
-
await asyncio.sleep(30)
|
|
207
|
-
try:
|
|
208
|
-
async with httpx.AsyncClient() as client:
|
|
209
|
-
await client.post(
|
|
210
|
-
f"{self.monitor.registry_url}/modules",
|
|
211
|
-
json={"action": "heartbeat", "module_id": "watchdog"},
|
|
212
|
-
headers={"Authorization": f"Bearer {self.monitor.own_token}"},
|
|
213
|
-
timeout=5,
|
|
214
|
-
)
|
|
215
|
-
except Exception:
|
|
216
|
-
pass
|
|
217
|
-
|
|
218
|
-
async def _test_event_loop(self):
|
|
219
|
-
"""Publish a test event every 5 seconds."""
|
|
220
|
-
from datetime import datetime, timezone
|
|
221
|
-
while True:
|
|
222
|
-
await asyncio.sleep(5)
|
|
223
|
-
await self._publish_event({
|
|
224
|
-
"event": "watchdog.test",
|
|
225
|
-
"data": {
|
|
226
|
-
"message": "heartbeat from watchdog",
|
|
227
|
-
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
228
|
-
},
|
|
229
|
-
})
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|