Lot 6 : bail du moteur côté Rust (chat::lease)

EngineLease prend le verrou de génération du Go, le renouvelle au tiers de
sa durée en tâche de fond et signale au tour un stop passé par le Go ou la
perte du bail (Reset, bascule, expiration). Rendu par release, ou en tâche
de fond s'il est lâché. engine_stop arrête un détenteur Go (tâche, bench,
optimiseur). Store::internal_post garde le statut HTTP pour distinguer les
refus.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
This commit is contained in:
Claude committed 2026-10-15 09:59:16 +00:00
1 parent 27ce303d7f
commit 091902039d
5 files changed
+377 -1

No files matched your search

+53
View File
@@ -32,6 +32,11 @@ pub struct FakeGo {
pub oai_log: Mutex<Vec<String>>,
/// Mémoire verrouillée : seal/open répondent 423.
pub locked: Mutex<bool>,
/// Bail du moteur : bail courant, stop signalé, détenteur Go, journal.
pub lease: Mutex<Option<String>>,
pub lease_stop: Mutex<bool>,
pub go_busy: Mutex<Option<String>>,
pub lease_log: Mutex<Vec<String>>,
/// Adresse d'écoute du faux Go (pour un client `Store` direct).
pub backend: Mutex<Option<SocketAddr>>,
}
@@ -118,6 +123,54 @@ async fn handle(st: Arc<FakeGo>, req: Request<Incoming>) -> Response<Full<Bytes>
resp(200, body.strip_prefix(b"SCELLE:").unwrap_or(&body).to_vec())
}
}
"/internal/v1/engine/lease" => {
let body = req.into_body().collect().await.unwrap().to_bytes();
let v: serde_json::Value = serde_json::from_slice(&body).unwrap_or_default();
if let Some(why) = st.go_busy.lock().unwrap().clone() {
let out = serde_json::json!({"error": why, "task_id": "t1", "task_name": "veille"});
return resp(409, out.to_string());
}
let mut lease = st.lease.lock().unwrap();
if lease.is_some() {
return resp(409, r#"{"error":"génération en cours"}"#);
}
let n = st.lease_log.lock().unwrap().len();
let id = format!("bail{n}");
*lease = Some(id.clone());
st.lease_log
.lock()
.unwrap()
.push(format!("acquire {}", v["ttl_ms"]));
resp(
200,
serde_json::json!({"lease": id, "ttl_ms": v["ttl_ms"]}).to_string(),
)
}
"/internal/v1/engine/lease/renew" | "/internal/v1/engine/lease/release" => {
let body = req.into_body().collect().await.unwrap().to_bytes();
let v: serde_json::Value = serde_json::from_slice(&body).unwrap_or_default();
let id = v["lease"].as_str().unwrap_or_default().to_string();
let mut lease = st.lease.lock().unwrap();
let held = lease.as_deref() == Some(id.as_str());
if path.ends_with("release") {
st.lease_log.lock().unwrap().push(format!("release {id}"));
if held {
*lease = None;
}
return resp(204, "");
}
st.lease_log.lock().unwrap().push(format!("renew {id}"));
if !held {
return resp(410, r#"{"error":"bail perdu"}"#);
}
let stop = *st.lease_stop.lock().unwrap();
resp(200, serde_json::json!({"stop": stop}).to_string())
}
"/internal/v1/engine/stop" => {
st.lease_log.lock().unwrap().push("stop".into());
*st.go_busy.lock().unwrap() = None;
resp(204, "")
}
"/internal/v1/tool/exec" => {
let body = req.into_body().collect().await.unwrap().to_bytes();
let v: serde_json::Value = serde_json::from_slice(&body).unwrap();
+96
View File
@@ -145,3 +145,99 @@ async fn index_illisible_jamais_reecrit() {
assert_eq!(opened(&go, BK_CHAT, "conv:c1").unwrap(), b"{}");
assert_eq!(go.get(BK_CHAT, CK_INDEX).unwrap(), "SCELLE:pas du json");
}
// ---- bail du moteur ----
use loki_rs::chat::lease::{EngineLease, LeaseError, LeaseSignal, engine_stop};
use std::time::Duration;
async fn until(mut f: impl FnMut() -> bool) {
tokio::time::timeout(Duration::from_secs(5), async {
while !f() {
tokio::time::sleep(Duration::from_millis(20)).await;
}
})
.await
.expect("condition jamais atteinte");
}
#[tokio::test]
async fn bail_pris_renouvele_rendu() {
let (st, go, _stop, _t) = store().await;
// TTL de 300 ms : renouvelé toutes les 100 ms.
let lease = EngineLease::acquire(st.clone(), Duration::from_millis(300))
.await
.unwrap();
assert_eq!(lease.id(), "bail0");
assert_eq!(go.lease_log.lock().unwrap()[0], "acquire 300");
// Un second bail est refusé tant que le premier tient.
match EngineLease::acquire(st.clone(), Duration::from_secs(30)).await {
Err(LeaseError::Busy { error, .. }) => assert_eq!(error, "génération en cours"),
other => panic!("second bail : {:?}", other.map(|l| l.id().to_string())),
}
until(|| {
go.lease_log
.lock()
.unwrap()
.iter()
.any(|l| l == "renew bail0")
})
.await;
assert_eq!(lease.signal(), LeaseSignal::Held);
lease.release().await;
assert_eq!(*go.lease.lock().unwrap(), None);
assert!(
go.lease_log
.lock()
.unwrap()
.contains(&"release bail0".to_string())
);
}
#[tokio::test]
async fn bail_stop_puis_perdu() {
let (st, go, _stop, _t) = store().await;
let mut lease = EngineLease::acquire(st, Duration::from_millis(300))
.await
.unwrap();
*go.lease_stop.lock().unwrap() = true;
let got = tokio::time::timeout(Duration::from_secs(5), lease.interrupted()).await;
assert_eq!(got.unwrap(), LeaseSignal::Stop);
// Le Go rompt le bail (Reset) : perdu.
*go.lease.lock().unwrap() = None;
until(|| lease.signal() == LeaseSignal::Lost).await;
assert_eq!(lease.interrupted().await, LeaseSignal::Lost);
}
#[tokio::test]
async fn bail_lache_sans_release_est_rendu() {
let (st, go, _stop, _t) = store().await;
let lease = EngineLease::acquire(st, Duration::from_secs(30))
.await
.unwrap();
drop(lease);
until(|| go.lease.lock().unwrap().is_none()).await;
}
#[tokio::test]
async fn moteur_tenu_par_une_tache_go() {
let (st, go, _stop, _t) = store().await;
*go.go_busy.lock().unwrap() = Some("la tâche planifiée « veille » occupe le modèle".into());
match EngineLease::acquire(st.clone(), Duration::from_secs(30)).await {
Err(LeaseError::Busy {
error,
task_id,
task_name,
}) => {
assert!(error.contains("veille"));
assert_eq!(task_id.as_deref(), Some("t1"));
assert_eq!(task_name.as_deref(), Some("veille"));
}
other => panic!("bail accordé : {:?}", other.map(|l| l.id().to_string())),
}
engine_stop(&st).await.unwrap();
let lease = EngineLease::acquire(st, Duration::from_secs(30))
.await
.unwrap();
lease.release().await;
}