fix(model-server): fiabiliser la readiness au démarrage d'un serveur modèle local (#55)
Le serveur modèle local (llama.cpp) était tué après ~5 s par un timeout de readiness prématuré, alors que le modèle était encore en warmup (chargement en RAM/VRAM). Résultat : « Error on loading local model » alors que le process était vivant et en train de démarrer normalement. Introduit une deadline de warmup longue configurable (~120 s via ReadinessPolicy) qui distingue un process « vivant en warmup » d'un process « mort » : - Ready → succès immédiat - Unreachable + Running → continuer d'attendre (warmup en cours) - Unreachable + Exited → échec rapide (process mort, inutile d'attendre) - deadline atteinte → stop + Timeout Tests (crates/application/tests/model_server.rs) : warmup lent, exit pendant le warmup, deadline atteinte, plus la régression adaptée. 20/20 verts, crate application verte. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -153,6 +153,8 @@ pub struct ReadinessPolicy {
|
||||
pub attempts: usize,
|
||||
/// Delay between attempts.
|
||||
pub backoff: Duration,
|
||||
/// Maximum time to wait for an auto-started process to expose its endpoint.
|
||||
pub warmup_deadline: Duration,
|
||||
}
|
||||
|
||||
impl Default for ReadinessPolicy {
|
||||
@ -160,6 +162,7 @@ impl Default for ReadinessPolicy {
|
||||
Self {
|
||||
attempts: 20,
|
||||
backoff: Duration::from_millis(250),
|
||||
warmup_deadline: Duration::from_secs(120),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -437,68 +440,9 @@ impl EnsureLocalModelServer {
|
||||
handle: &ManagedProcessHandle,
|
||||
hf_source: Option<String>,
|
||||
) -> Result<EnsureLocalModelServerOutput, AppError> {
|
||||
for attempt in 0..self.readiness.attempts {
|
||||
match self.probe.probe(&config.endpoint).await {
|
||||
Err(err) => {
|
||||
self.stop_started_server(config.id, handle).await;
|
||||
return self.fail(config.id, err);
|
||||
}
|
||||
Ok(ModelServerStatus::ReadyReused | ModelServerStatus::ReadyStarted) => {
|
||||
self.publish(
|
||||
config.id,
|
||||
ModelServerLifecycleStatus::Ready { reused: false },
|
||||
);
|
||||
return Ok(EnsureLocalModelServerOutput {
|
||||
ready: ready(config, ModelServerStatus::ReadyStarted),
|
||||
});
|
||||
}
|
||||
Ok(ModelServerStatus::Unreachable) => {
|
||||
if attempt + 1 < self.readiness.attempts && !self.readiness.backoff.is_zero() {
|
||||
tokio::time::sleep(self.readiness.backoff).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let Some(source) = hf_source else {
|
||||
let err = ModelServerError::Timeout;
|
||||
self.stop_started_server(config.id, handle).await;
|
||||
return self.fail(config.id, err);
|
||||
};
|
||||
|
||||
match self.process.status(handle).await {
|
||||
Ok(ProcessStatus::Running) => {
|
||||
self.publish(
|
||||
config.id,
|
||||
ModelServerLifecycleStatus::Downloading {
|
||||
downloaded_bytes: None,
|
||||
total_bytes: None,
|
||||
percent: None,
|
||||
source: Some(source.clone()),
|
||||
},
|
||||
);
|
||||
}
|
||||
Ok(ProcessStatus::Exited { code }) => {
|
||||
self.active.lock().unwrap().remove(&config.id);
|
||||
return self.fail(config.id, premature_exit_error(code));
|
||||
}
|
||||
Ok(ProcessStatus::Unknown) => {
|
||||
self.active.lock().unwrap().remove(&config.id);
|
||||
return self.fail(
|
||||
config.id,
|
||||
ModelServerError::Process("process status unknown".to_owned()),
|
||||
);
|
||||
}
|
||||
Err(err) => return self.fail(config.id, err),
|
||||
}
|
||||
|
||||
let deadline = Instant::now() + self.hf_download_deadline;
|
||||
let mut attempts = 0usize;
|
||||
let deadline = Instant::now() + self.readiness.warmup_deadline;
|
||||
loop {
|
||||
if Instant::now() >= deadline {
|
||||
let err = ModelServerError::Timeout;
|
||||
self.stop_started_server(config.id, handle).await;
|
||||
return self.fail(config.id, err);
|
||||
}
|
||||
match self.probe.probe(&config.endpoint).await {
|
||||
Err(err) => {
|
||||
self.stop_started_server(config.id, handle).await;
|
||||
@ -515,12 +459,21 @@ impl EnsureLocalModelServer {
|
||||
}
|
||||
Ok(ModelServerStatus::Unreachable) => {}
|
||||
}
|
||||
|
||||
match self.process.status(handle).await {
|
||||
Ok(ProcessStatus::Running) => {
|
||||
if !self.readiness.backoff.is_zero() {
|
||||
tokio::time::sleep(self.readiness.backoff).await;
|
||||
} else {
|
||||
tokio::task::yield_now().await;
|
||||
if attempts.saturating_add(1) == self.readiness.attempts {
|
||||
if let Some(source) = hf_source.as_ref() {
|
||||
self.publish(
|
||||
config.id,
|
||||
ModelServerLifecycleStatus::Downloading {
|
||||
downloaded_bytes: None,
|
||||
total_bytes: None,
|
||||
percent: None,
|
||||
source: Some(source.clone()),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(ProcessStatus::Exited { code }) => {
|
||||
@ -536,6 +489,18 @@ impl EnsureLocalModelServer {
|
||||
}
|
||||
Err(err) => return self.fail(config.id, err),
|
||||
}
|
||||
|
||||
attempts = attempts.saturating_add(1);
|
||||
if Instant::now() >= deadline {
|
||||
let err = ModelServerError::Timeout;
|
||||
self.stop_started_server(config.id, handle).await;
|
||||
return self.fail(config.id, err);
|
||||
}
|
||||
if !self.readiness.backoff.is_zero() {
|
||||
tokio::time::sleep(self.readiness.backoff).await;
|
||||
} else {
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user