Entropic 2.11.1
Local-first agentic inference engine
Loading...
Searching...
No Matches
health_monitor.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
10
11static auto logger = entropic::log::get("mcp.health_monitor");
12
13namespace entropic {
14
23 ReconnectPolicy policy,
24 uint32_t health_check_interval_ms)
25 : policy_(std::move(policy)),
26 health_check_interval_ms_(health_check_interval_ms) {}
27
36
45 const std::string& name,
46 ExternalMCPClient* client) {
47
48 std::lock_guard<std::mutex> lock(watched_mutex_);
49 WatchEntry entry;
50 entry.client = client;
51 entry.status = client->is_connected() ? "connected" : "disconnected";
52 entry.reconnect_attempt = 0;
53 entry.next_action = std::chrono::steady_clock::now();
54 watched_[name] = entry;
55
56 logger->info("Watching server '{}'", name);
57 wake_cv_.notify_one();
58}
59
66void HealthMonitor::unwatch(const std::string& name) {
67 std::lock_guard<std::mutex> lock(watched_mutex_);
68 watched_.erase(name);
69 logger->info("Unwatched server '{}'", name);
70}
71
79 status_callback_ = std::move(cb);
80}
81
88 if (running_) {
89 return;
90 }
91 running_ = true;
92 monitor_thread_ = std::thread(
93 &HealthMonitor::monitor_loop, this);
94 logger->info("Health monitor started");
95}
96
103 if (!running_.exchange(false)) {
104 return;
105 }
106 wake_cv_.notify_all();
107 if (monitor_thread_.joinable()) {
108 monitor_thread_.join();
109 }
110 logger->info("Health monitor stopped");
111}
112
125 std::vector<HealthEvent> events;
126 {
127 std::lock_guard<std::mutex> lock(event_mutex_);
128 events.swap(event_queue_);
129 }
130
131 for (const auto& event : events) {
132 if (status_callback_) {
133 status_callback_(event);
134 }
135 }
136}
137
143void HealthMonitor::monitor_loop() {
144 constexpr auto poll_interval = std::chrono::milliseconds(500);
145
146 while (running_) {
147 {
148 std::lock_guard<std::mutex> lock(watched_mutex_);
149 auto now = std::chrono::steady_clock::now();
150 for (auto& [name, entry] : watched_) {
151 if (now >= entry.next_action) {
152 check_server(name, entry);
153 }
154 }
155 }
156
157 std::unique_lock<std::mutex> lock(wake_mutex_);
158 wake_cv_.wait_for(lock, poll_interval,
159 [this] { return !running_.load(); });
160 }
161}
162
174void HealthMonitor::check_server(
175 const std::string& name,
176 WatchEntry& entry) {
177
178 bool alive = entry.client->is_connected();
179
180 if (entry.status == "connected" && !alive) {
181 // Connection lost — start reconnecting
182 auto old = entry.status;
183 entry.status = "disconnected";
184 entry.reconnect_attempt = 0;
185 post_event(name, old, entry.status);
186 logger->warn("Server '{}' disconnected", name);
187 attempt_reconnect(name, entry);
188 return;
189 }
190
191 if (entry.status == "disconnected" ||
192 entry.status == "reconnecting") {
193 attempt_reconnect(name, entry);
194 return;
195 }
196
197 // Connected + healthy: schedule next check
198 if (health_check_interval_ms_ > 0) {
199 entry.next_action = std::chrono::steady_clock::now() +
200 std::chrono::milliseconds(health_check_interval_ms_);
201 } else {
202 entry.next_action = std::chrono::steady_clock::now() +
203 std::chrono::seconds(5);
204 }
205}
206
219void HealthMonitor::attempt_reconnect(
220 const std::string& name,
221 WatchEntry& entry) {
222
223 if (policy_.exhausted(entry.reconnect_attempt)) {
224 if (entry.status != "error") {
225 auto old = entry.status;
226 entry.status = "error";
227 post_event(name, old, "error");
228 logger->error("Server '{}' reconnection exhausted "
229 "after {} attempts",
230 name, entry.reconnect_attempt);
231 }
232 entry.next_action = std::chrono::steady_clock::time_point::max();
233 return;
234 }
235
236 if (entry.status != "reconnecting") {
237 auto old = entry.status;
238 entry.status = "reconnecting";
239 post_event(name, old, "reconnecting");
240 }
241
242 logger->info("Reconnecting to '{}' (attempt {})",
243 name, entry.reconnect_attempt + 1);
244
245 if (entry.client->connect()) {
246 on_reconnect_success(name, entry);
247 return;
248 }
249
250 // Schedule next attempt with backoff
251 auto delay = policy_.delay_ms(entry.reconnect_attempt);
252 entry.reconnect_attempt++;
253 entry.next_action = std::chrono::steady_clock::now() +
254 std::chrono::milliseconds(delay);
255
256 logger->info("Server '{}' reconnect failed, "
257 "retry in {}ms", name, delay);
258}
259
273void HealthMonitor::on_reconnect_success(const std::string& name,
274 WatchEntry& entry) {
275 auto [added, removed] = entry.client->refresh_tools();
276 entry.status = "connected";
277 entry.reconnect_attempt = 0;
278
279 HealthEvent evt;
280 evt.server_name = name;
281 evt.old_status = "reconnecting";
282 evt.new_status = "connected";
283 evt.added_tools = std::move(added);
284 evt.removed_tools = std::move(removed);
285 {
286 std::lock_guard<std::mutex> lock(event_mutex_);
287 event_queue_.push_back(std::move(evt));
288 }
289
290 logger->info("Server '{}' reconnected", name);
291 entry.next_action = std::chrono::steady_clock::now();
292}
293
306void HealthMonitor::post_event(
307 const std::string& name,
308 const std::string& old_status,
309 const std::string& new_status) {
310
311 HealthEvent evt;
312 evt.server_name = name;
313 evt.old_status = old_status;
314 evt.new_status = new_status;
315 {
316 std::lock_guard<std::mutex> lock(event_mutex_);
317 event_queue_.push_back(std::move(evt));
318 }
319}
320
321} // namespace entropic
Client for an external MCP server (stdio or SSE).
bool is_connected() const
Check connection state.
void start()
Start the monitoring thread.
void watch(const std::string &name, ExternalMCPClient *client)
Start monitoring a server.
void stop()
Stop monitoring and all reconnection attempts.
HealthMonitor(ReconnectPolicy policy, uint32_t health_check_interval_ms=0)
Construct with reconnection policy.
std::function< void(const HealthEvent &)> StatusCallback
Callback invoked on engine thread when processing events.
void process_events()
Drain event queue, invoke callbacks (call on engine thread).
~HealthMonitor()
Destructor — stops monitor if running.
void set_status_callback(StatusCallback cb)
Set callback for status change events.
void unwatch(const std::string &name)
Stop monitoring a server.
Exponential backoff with jitter for reconnection attempts.
bool exhausted(uint32_t attempt) const
Check if retries are exhausted.
uint32_t delay_ms(uint32_t attempt) const
Compute delay for the given attempt number.
Monitors external MCP server health and triggers reconnection.
spdlog initialization and logger access.
ENTROPIC_EXPORT std::shared_ptr< spdlog::logger > get(const std::string &name)
Get or create a named logger.
Definition logging.cpp:211
Activate model on GPU (WARM → ACTIVE).