Lazily initialize PulseClient in ChatEngine with OnceLock
Pulse telemetry client is now created on first async use, not in the constructor. This avoids requiring a Tokio runtime at construction time and ensures correct initialization in UniFFI contexts.
keypair34 committed
Apr 28, 2026 at 20:34 UTC
f1bdf2b7adae2264eac42ac9d857837fb4a7b3f0
1 file changed
+33
-18
src/inference/engine.rs
+33
-18
@@ -242,7 +242,7 @@ enum HistoryEntry {
242
))]
243
pub struct ChatEngine {
244
inner: Mutex<Option<LoadedModel>>,
245
- pulse: Option<crate::pulse::PulseClient>,
245
+ pulse: std::sync::OnceLock<Option<crate::pulse::PulseClient>>,
246
}
247
248
#[cfg(any(
@@ -260,26 +260,41 @@ impl ChatEngine {
260
261
/// Create a new engine with no model loaded.
262
pub fn new() -> Self {
263
- let environment = Self::pulse_environment();
264
- let edge_id = std::env::var("ONDE_EDGE_ID").unwrap_or_else(|_| "onde-unknown".to_string());
265
- let pulse = crate::pulse::PulseClient::new(environment, edge_id);
266
-
267
- match &pulse {
268
- Some(_) => {
269
- log::info!("ChatEngine: pulse telemetry enabled (environment={environment})")
270
- }
271
- None => log::info!(
272
- "ChatEngine: pulse telemetry disabled \
273
- (GRESIQ_API_KEY / GRESIQ_API_SECRET not embedded at SDK build time)"
274
- ),
275
- }
276
-
263
Self {
264
inner: Mutex::new(None),
279
- pulse,
265
+ pulse: std::sync::OnceLock::new(),
266
}
267
}
268
269
+ /// Lazily initialise the pulse telemetry client.
270
+ ///
271
+ /// The underlying GresIQ SDK creates a `reqwest::Client`, which needs
272
+ /// a Tokio runtime. `ChatEngine::new()` is called from a sync UniFFI
273
+ /// constructor where no runtime exists yet, so we defer creation to the
274
+ /// first async call site (e.g. `load_gguf_model` or `send_message`).
275
+ fn pulse(&self) -> Option<&crate::pulse::PulseClient> {
276
+ self.pulse
277
+ .get_or_init(|| {
278
+ let environment = Self::pulse_environment();
279
+ let edge_id =
280
+ std::env::var("ONDE_EDGE_ID").unwrap_or_else(|_| "onde-unknown".to_string());
281
+ let client = crate::pulse::PulseClient::new(environment, edge_id);
282
+
283
+ match &client {
284
+ Some(_) => log::info!(
285
+ "ChatEngine: pulse telemetry enabled (environment={environment})"
286
+ ),
287
+ None => log::info!(
288
+ "ChatEngine: pulse telemetry disabled \
289
+ (GRESIQ_API_KEY / GRESIQ_API_SECRET not embedded at SDK build time)"
290
+ ),
291
+ }
292
+
293
+ client
294
+ })
295
+ .as_ref()
296
+ }
297
+
298
/// Resolve the pulse environment from the `GRESIQ_ENVIRONMENT` env var.
299
/// Defaults to `Production` when the var is absent or unrecognised.
300
fn pulse_environment() -> smbcloud_gresiq_sdk::Environment {
@@ -456,7 +471,7 @@ impl ChatEngine {
471
};
472
drop(old_model); // free old weights outside the lock
473
459
- if let Some(ref pulse) = self.pulse {
474
+ if let Some(pulse) = self.pulse() {
475
pulse
476
.record_model_loaded(pulse_model_id, pulse_model_name, elapsed.as_millis() as u64)
477
.await;
@@ -867,7 +882,7 @@ impl ChatEngine {
882
}
883
}
884
870
- if let Some(ref pulse) = self.pulse {
885
+ if let Some(pulse) = self.pulse() {
886
pulse.record_inference(
887
pulse_model_id,
888
crate::pulse::next_request_id(),