@ondeinference / onde / commits / 2a2799c

Make record_model_loaded synchronous and await completion

PulseClient::record_model_loaded is now async and awaited during model load, ensuring the model_loaded row exists before any inference events are sent.

Seto Elkahfi committed May 9, 2026 at 11:20 UTC 2a2799c55d1e0c0851bc2942540e1ac1209cbadb
2 files changed +29 -19
src/inference/engine.rs
+3 -1
@@ -457,7 +457,9 @@ impl ChatEngine {
457 drop(old_model); // free old weights outside the lock
458
459 if let Some(ref pulse) = self.pulse {
460 - pulse.record_model_loaded(pulse_model_id, pulse_model_name, elapsed.as_millis() as u64);
460 + pulse
461 + .record_model_loaded(pulse_model_id, pulse_model_name, elapsed.as_millis() as u64)
462 + .await;
463 }
464
465 Ok(elapsed)
src/pulse/client.rs
+26 -18
@@ -59,25 +59,33 @@ impl PulseClient {
59 Some(PulseClient { inner, edge_id })
60 }
61
62 - /// Spawns a background task that writes the model-load event to the
63 - /// pulse/model_loaded table, then returns immediately. Slow network
64 - /// responses don't block the first inference request.
62 + /// Writes the model-load event to the pulse/model_loaded table and
63 + /// **awaits** the result before returning.
64 ///
66 - /// Failed writes emit a warn! log line — no retry, no queue,
67 - /// and no effect on the caller.
68 - pub fn record_model_loaded(&self, model_id: String, model_name: String, load_duration_ms: u64) {
69 - let client = self.clone();
70 - tokio::spawn(async move {
71 - let event = ModelLoadedEvent {
72 - edge_id: client.edge_id.clone(),
73 - model_id,
74 - model_name,
75 - load_duration_ms,
76 - };
77 - if let Err(error) = client.inner.insert("pulse/model_loaded", &event).await {
78 - log::warn!("pulse: model_loaded failed: {}", error);
79 - }
80 - });
65 + /// This must complete before any `record_inference` call for the same
66 + /// edge + model is made, because the API enforces a foreign-key
67 + /// constraint: an `inference_event` row is rejected with HTTP 404
68 + /// ("Call model_loaded first") if no matching `model_loaded` row exists.
69 + ///
70 + /// Keeping this synchronous with respect to the model-load path means
71 + /// the row is always present by the time the first inference event fires.
72 + /// A failed write emits a warn! log line — no retry, no queue, and no
73 + /// effect on the caller.
74 + pub async fn record_model_loaded(
75 + &self,
76 + model_id: String,
77 + model_name: String,
78 + load_duration_ms: u64,
79 + ) {
80 + let event = ModelLoadedEvent {
81 + edge_id: self.edge_id.clone(),
82 + model_id,
83 + model_name,
84 + load_duration_ms,
85 + };
86 + if let Err(error) = self.inner.insert("pulse/model_loaded", &event).await {
87 + log::warn!("pulse: model_loaded failed: {}", error);
88 + }
89 }
90
91 /// Same fire-and-forget pattern as record_model_loaded but for inference