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