fix(model-server): deadline de warmup configurable pour le cold-start llama.cpp (#55)
Corrige la régression high « Error on loading local model » : le premier chargement du serveur modèle local (cold-start llama.cpp) dépassait la fenêtre de readiness et échouait. Le warmup dispose désormais d'un deadline par défaut de 600 s, surchargable par config optionnelle `warmup_deadline_secs` (validée dans [30, 1800]). - domain: champ `warmup_deadline_secs: Option<u64>` + validation de borne - application: policy de readiness effective (défaut 600 s, override par config) - app-tauri: DTO `warmupDeadlineSecs` - infrastructure: application de la deadline effective au warmup Verdict QA (vert) : domain 252, application 81 + 22 model_server, app-tauri dto_model_servers 6, infrastructure model_server 2, build OK. Contrat readiness couvert sur ports mockés. Caveat : le cold-start end-to-end réel (llama-server) sort du sandbox de test -> vérification manuelle utilisateur restante, non couverte par les tests unitaires. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -162,7 +162,7 @@ impl Default for ReadinessPolicy {
|
||||
Self {
|
||||
attempts: 20,
|
||||
backoff: Duration::from_millis(250),
|
||||
warmup_deadline: Duration::from_secs(120),
|
||||
warmup_deadline: Duration::from_secs(600),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -261,6 +261,15 @@ impl EnsureLocalModelServer {
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns the readiness policy effective for a persisted server config.
|
||||
///
|
||||
/// The config may override only the warmup deadline; probe cadence remains
|
||||
/// owned by the application policy.
|
||||
#[must_use]
|
||||
pub fn effective_readiness_policy(&self, config: &LocalModelServerConfig) -> ReadinessPolicy {
|
||||
self.readiness_for(config)
|
||||
}
|
||||
|
||||
/// Overrides the long Hugging Face download/preparation deadline.
|
||||
#[must_use]
|
||||
pub fn with_hf_download_deadline(mut self, deadline: Duration) -> Self {
|
||||
@ -440,8 +449,9 @@ impl EnsureLocalModelServer {
|
||||
handle: &ManagedProcessHandle,
|
||||
hf_source: Option<String>,
|
||||
) -> Result<EnsureLocalModelServerOutput, AppError> {
|
||||
let readiness = self.readiness_for(config);
|
||||
let mut attempts = 0usize;
|
||||
let deadline = Instant::now() + self.readiness.warmup_deadline;
|
||||
let deadline = Instant::now() + readiness.warmup_deadline;
|
||||
loop {
|
||||
match self.probe.probe(&config.endpoint).await {
|
||||
Err(err) => {
|
||||
@ -462,7 +472,7 @@ impl EnsureLocalModelServer {
|
||||
|
||||
match self.process.status(handle).await {
|
||||
Ok(ProcessStatus::Running) => {
|
||||
if attempts.saturating_add(1) == self.readiness.attempts {
|
||||
if attempts.saturating_add(1) == readiness.attempts {
|
||||
if let Some(source) = hf_source.as_ref() {
|
||||
self.publish(
|
||||
config.id,
|
||||
@ -496,14 +506,24 @@ impl EnsureLocalModelServer {
|
||||
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;
|
||||
if !readiness.backoff.is_zero() {
|
||||
tokio::time::sleep(readiness.backoff).await;
|
||||
} else {
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn readiness_for(&self, config: &LocalModelServerConfig) -> ReadinessPolicy {
|
||||
ReadinessPolicy {
|
||||
warmup_deadline: config
|
||||
.warmup_deadline_secs
|
||||
.map(Duration::from_secs)
|
||||
.unwrap_or(self.readiness.warmup_deadline),
|
||||
..self.readiness
|
||||
}
|
||||
}
|
||||
|
||||
/// Stops active servers whose policy is [`StopPolicy::StopOnAppExit`].
|
||||
///
|
||||
/// # Errors
|
||||
|
||||
@ -435,6 +435,43 @@ fn progress(downloaded: Option<u64>, total: Option<u64>) -> ModelArtifactProgres
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn readiness_policy_default_warmup_deadline_is_ten_minutes() {
|
||||
assert_eq!(
|
||||
ModelServerReadinessPolicy::default().warmup_deadline,
|
||||
Duration::from_secs(600)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn effective_readiness_policy_uses_configured_warmup_deadline_when_present() {
|
||||
let configured = config(sid(23), 8103, "/models/qwen.gguf", true)
|
||||
.with_warmup_deadline_secs(Some(900))
|
||||
.unwrap();
|
||||
let unconfigured = config(sid(24), 8104, "/models/qwen.gguf", true);
|
||||
let usecase = EnsureLocalModelServer::new(
|
||||
Arc::new(FakeRegistry::default()),
|
||||
Arc::new(FakeProbe::new(Vec::new())),
|
||||
Arc::new(FakeProcess::default()),
|
||||
Arc::new(FakeRuntime),
|
||||
Arc::new(FakeFs::default()),
|
||||
Arc::new(FakeEvents::default()),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
usecase
|
||||
.effective_readiness_policy(&configured)
|
||||
.warmup_deadline,
|
||||
Duration::from_secs(900)
|
||||
);
|
||||
assert_eq!(
|
||||
usecase
|
||||
.effective_readiness_policy(&unconfigured)
|
||||
.warmup_deadline,
|
||||
Duration::from_secs(600)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn reachable_server_is_reused_without_spawn() {
|
||||
let registry = Arc::new(FakeRegistry::default());
|
||||
|
||||
Reference in New Issue
Block a user