Coverage for agentos/plugins/lifecycle.py: 0%

165 statements  

« prev     ^ index     » next       coverage.py v7.14.3, created at 2026-07-06 10:59 +0800

1""" 

2AgentOS v0.70 — 插件生命周期管理器。 

3基因来源: Kubernetes Pod Lifecycle + Spring Boot Actuator 

4 

5生命周期钩子: 

6- on_load() → 插件加载完成 

7- on_init(config) → 初始化配置 

8- on_start() → 开始工作 

9- on_stop() → 优雅关闭 

10- on_error(e) → 异常处理 

11- health_check() → 健康检查 

12""" 

13 

14from __future__ import annotations 

15 

16import asyncio 

17import time 

18from abc import ABC 

19from dataclasses import dataclass, field 

20 

21from agentos.plugins.registry import PluginRegistry, RegisteredPlugin, PluginStatus 

22 

23 

24# ── Abstract Plugin Base ───────────────────────── 

25 

26class LifecyclePlugin(ABC): 

27 """插件基类 — 实现标准生命周期钩子。""" 

28 

29 def __init__(self, config: dict | None = None): 

30 self.config = config or {} 

31 self._started_at: float = 0.0 

32 self._error_count: int = 0 

33 

34 @property 

35 def uptime_seconds(self) -> float: 

36 if self._started_at == 0: 

37 return 0.0 

38 return time.time() - self._started_at 

39 

40 async def on_load(self): 

41 """插件加载后调用。""" 

42 

43 async def on_init(self, config: dict): 

44 """初始化配置后调用。""" 

45 self.config = config 

46 

47 async def on_start(self): 

48 """开始工作时调用。""" 

49 self._started_at = time.time() 

50 

51 async def on_stop(self): 

52 """优雅关闭时调用。""" 

53 self._started_at = 0.0 

54 

55 async def on_error(self, error: Exception) -> bool: 

56 """ 

57 异常处理钩子。返回True表示已处理(可恢复),False表示致命错误。 

58 """ 

59 self._error_count += 1 

60 return False 

61 

62 async def health_check(self) -> HealthStatus: 

63 """健康检查 — 子类可覆盖。""" 

64 return HealthStatus.HEALTHY 

65 

66 

67@dataclass 

68class PluginHealth: 

69 """插件健康状态摘要。""" 

70 plugin_name: str 

71 status: str 

72 uptime_seconds: float 

73 error_count: int 

74 last_error: str 

75 

76 

77class HealthStatus: 

78 """插件健康状态。""" 

79 status: str = "healthy" # healthy | degraded | unhealthy 

80 details: dict = field(default_factory=dict) 

81 uptime_seconds: float = 0.0 

82 error_count: int = 0 

83 

84 @property 

85 def is_healthy(self) -> bool: 

86 return self.status == "healthy" 

87 

88 def to_dict(self) -> dict: 

89 return { 

90 "status": self.status, 

91 "details": self.details, 

92 "uptime_seconds": self.uptime_seconds, 

93 "error_count": self.error_count, 

94 } 

95 

96 

97from dataclasses import dataclass, field 

98 

99 

100@dataclass 

101class LifecycleReport: 

102 """插件生命周期报告。""" 

103 

104 plugin_name: str 

105 status: PluginStatus 

106 load_time_ms: float = 0.0 

107 init_time_ms: float = 0.0 

108 uptime_seconds: float = 0.0 

109 health: HealthStatus | None = None 

110 error: str | None = None 

111 

112 

113# ── Lifecycle Manager ──────────────────────────── 

114 

115class LifecycleManager: 

116 """ 

117 生命周期管理器 — 协调所有插件的init/start/stop。 

118 支持: 批量初始化、健康检查轮询、优雅降级。 

119 """ 

120 

121 def __init__(self, registry: PluginRegistry): 

122 self.registry = registry 

123 self._reports: dict[str, LifecycleReport] = {} 

124 self._health_check_task: asyncio.Task | None = None 

125 

126 async def init_all(self, configs: dict[str, dict] | None = None): 

127 """初始化所有已注册的LOADED状态插件。""" 

128 configs = configs or {} 

129 plugins = self.registry.by_status(PluginStatus.LOADED) 

130 

131 for rp in plugins: 

132 if rp.instance and isinstance(rp.instance, LifecyclePlugin): 

133 start = time.time() 

134 try: 

135 cfg = configs.get(rp.manifest.name, {}) 

136 await rp.instance.on_init(cfg) 

137 rp.status = PluginStatus.INITIALIZED 

138 init_ms = (time.time() - start) * 1000 

139 self._reports[rp.manifest.name] = LifecycleReport( 

140 plugin_name=rp.manifest.name, 

141 status=PluginStatus.INITIALIZED, 

142 load_time_ms=rp.load_time_ms, 

143 init_time_ms=init_ms, 

144 ) 

145 except Exception as e: 

146 rp.status = PluginStatus.ERROR 

147 rp.error = str(e) 

148 await self._handle_error(rp, e) 

149 

150 async def start_all(self): 

151 """启动所有INITIALIZED状态的插件。""" 

152 plugins = self.registry.by_status(PluginStatus.INITIALIZED) 

153 

154 for rp in plugins: 

155 await self.start_one(rp.manifest.name) 

156 

157 async def start_one(self, name: str) -> LifecycleReport: 

158 rp = self.registry.get(name) 

159 if not rp: 

160 raise KeyError(f"Plugin '{name}' not found") 

161 if rp.status not in (PluginStatus.INITIALIZED, PluginStatus.STOPPED): 

162 raise RuntimeError(f"Cannot start plugin '{name}' in status {rp.status}") 

163 

164 try: 

165 if rp.instance and isinstance(rp.instance, LifecyclePlugin): 

166 await rp.instance.on_start() 

167 rp.status = PluginStatus.ACTIVE 

168 rp.error = None 

169 report = self._reports.get(name, LifecycleReport(plugin_name=name, status=PluginStatus.ACTIVE)) 

170 report.status = PluginStatus.ACTIVE 

171 report.uptime_seconds = rp.instance.uptime_seconds if rp.instance and isinstance(rp.instance, LifecyclePlugin) else 0 

172 self._reports[name] = report 

173 return report 

174 except Exception as e: 

175 rp.status = PluginStatus.ERROR 

176 rp.error = str(e) 

177 return LifecycleReport(plugin_name=name, status=PluginStatus.ERROR, error=str(e)) 

178 

179 async def stop_all(self, graceful: bool = True): 

180 """停止所有ACTIVE插件。""" 

181 plugins = self.registry.by_status(PluginStatus.ACTIVE) 

182 

183 for rp in plugins: 

184 await self.stop_one(rp.manifest.name, graceful) 

185 

186 async def stop_one(self, name: str, graceful: bool = True): 

187 rp = self.registry.get(name) 

188 if not rp: 

189 return 

190 

191 rp.status = PluginStatus.STOPPING 

192 try: 

193 if rp.instance and isinstance(rp.instance, LifecyclePlugin): 

194 await rp.instance.on_stop() 

195 except Exception as e: 

196 rp.error = str(e) 

197 finally: 

198 rp.status = PluginStatus.STOPPED 

199 if name in self._reports: 

200 self._reports[name].status = PluginStatus.STOPPED 

201 

202 async def health_check_all(self) -> dict[str, HealthStatus]: 

203 """对所有ACTIVE插件执行健康检查。""" 

204 results = {} 

205 plugins = self.registry.by_status(PluginStatus.ACTIVE) 

206 

207 for rp in plugins: 

208 if rp.instance and isinstance(rp.instance, LifecyclePlugin): 

209 try: 

210 health = await rp.instance.health_check() 

211 except Exception as e: 

212 health = HealthStatus( 

213 status="unhealthy", 

214 details={"error": str(e)}, 

215 error_count=rp.instance._error_count if rp.instance else 0, 

216 ) 

217 health.uptime_seconds = rp.instance.uptime_seconds if rp.instance else 0 

218 results[rp.manifest.name] = health 

219 

220 return results 

221 

222 def start_health_polling(self, interval_seconds: float = 30.0): 

223 """启动后台健康检查轮询。""" 

224 async def _poll(): 

225 while True: 

226 try: 

227 await self.health_check_all() 

228 except Exception: 

229 pass 

230 await asyncio.sleep(interval_seconds) 

231 

232 self._health_check_task = asyncio.ensure_future(_poll()) 

233 

234 def stop_health_polling(self): 

235 if self._health_check_task: 

236 self._health_check_task.cancel() 

237 self._health_check_task = None 

238 

239 def report(self) -> list[LifecycleReport]: 

240 """获取所有插件的生命周期报告。""" 

241 reports = list(self._reports.values()) 

242 # Add any not tracked in _reports 

243 tracked = {r.plugin_name for r in reports} 

244 for rp in self.registry.list_all(): 

245 if rp.manifest.name not in tracked: 

246 reports.append(LifecycleReport( 

247 plugin_name=rp.manifest.name, 

248 status=rp.status, 

249 load_time_ms=rp.load_time_ms, 

250 error=rp.error, 

251 )) 

252 return reports 

253 

254 def summary(self) -> str: 

255 reports = self.report() 

256 lines = [f"共 {len(reports)} 个插件"] 

257 for r in reports: 

258 lines.append(f" [{r.status.value}] {r.plugin_name} (load:{r.load_time_ms:.0f}ms, init:{r.init_time_ms:.0f}ms)") 

259 if r.error: 

260 lines.append(f" error: {r.error}") 

261 return "\n".join(lines) 

262 

263 async def _handle_error(self, rp: RegisteredPlugin, error: Exception): 

264 if rp.instance and isinstance(rp.instance, LifecyclePlugin): 

265 try: 

266 handled = await rp.instance.on_error(error) 

267 if handled: 

268 rp.error = None 

269 except Exception: 

270 pass