mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-17 12:16:21 +02:00
Port de backend_strata.go (rust/loki-rs/src/strata) : la machine (cartes par nvidia-smi, Python 3.10+ et venv, RAM totale et disponible, RAM du moteur Strata en marche, place libre), ce qui y tient et la recommandation, la configuration de lancement en clair des modèles installés (placement des experts drop/ram/arena/disk, carte d'aide, réglages du preset), les réglages d'un modèle installé et l'installation (job des moteurs : paquet figé vérifié par SHA-256, installeur lancé et journalisé comme le sinkWriter du Go, preset écrit puis activé par le Go). Tenu seulement avec LOKI_BACKENDS=rust ; le lancement (loki serve) reste au Go, qui relaie les routes du tunnel. Correction du Go : /api/strata/settings décidait « en service » après avoir écrit les nouveaux réglages, donc jamais pour un réglage changé ; décidé désormais avant l'écriture, comme SavePresetApplying. Le port suit. Parité : parity_strata_test.go (crochets de test : release, meminfo, place libre, action de service) tire du Go un journal rejoué par tests/strata.rs ; les tests du Go portés dans strata/tests.rs ; TestFacadeStrata côté Go. Mutations : 71 tuées sur 71. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
357 lines
12 KiB
Rust
357 lines
12 KiB
Rust
//! Lot 25 : Strata tenu par la façade (`LOKI_STRATA=rust`, avec
|
|
//! `LOKI_BACKENDS=rust`).
|
|
//!
|
|
//! Le journal tiré du Go (`internal/loki/parity_strata_test.go` →
|
|
//! `tests/parity_strata/strata.json`) est rejoué par une façade : fausses
|
|
//! machines dans le PATH des sondes (nvidia-smi, python3), faux /proc/meminfo,
|
|
//! place libre imposée, fausse release (sources, moteur, empreintes) et
|
|
//! installeur factice ; la bascule finale part au faux Go, qui applique le
|
|
//! preset comme `applyPresetFile`.
|
|
|
|
mod common;
|
|
|
|
use std::collections::BTreeMap;
|
|
use std::convert::Infallible;
|
|
use std::net::SocketAddr;
|
|
use std::path::{Path, PathBuf};
|
|
use std::sync::{Arc, Mutex};
|
|
use std::time::Duration;
|
|
|
|
use bytes::Bytes;
|
|
use common::fakego;
|
|
use common::parity::{apply_files, tar_gz, write_file};
|
|
use http_body_util::Full;
|
|
use hyper::service::service_fn;
|
|
use hyper::{Request, Response};
|
|
use hyper_util::rt::TokioIo;
|
|
use loki_rs::backends::Backends;
|
|
use loki_rs::backends::probe::Probe;
|
|
use loki_rs::strata::api::Strata;
|
|
use regex::Regex;
|
|
use serde_json::{Value, json};
|
|
use sha2::Digest;
|
|
use tokio::net::TcpListener;
|
|
|
|
fn fixture() -> Value {
|
|
serde_json::from_str(include_str!("parity_strata/strata.json")).expect("journal")
|
|
}
|
|
|
|
fn s(v: &Value) -> &str {
|
|
v.as_str().unwrap_or("")
|
|
}
|
|
|
|
const TUNE_BUSY: &str = "optimisation en cours (loki tune) : le moteur est réservé aux essais — attends la fin ou annule-la";
|
|
|
|
/// La fausse release : `{status, src, engine}`.
|
|
async fn fake_release(rel: Arc<Mutex<Value>>) -> String {
|
|
let l = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
|
let addr = l.local_addr().unwrap();
|
|
tokio::spawn(async move {
|
|
while let Ok((st, _)) = l.accept().await {
|
|
let rel = rel.clone();
|
|
tokio::spawn(async move {
|
|
let svc = service_fn(move |req: Request<hyper::body::Incoming>| {
|
|
let cur = rel.lock().unwrap().clone();
|
|
async move {
|
|
let path = req.uri().path().trim_start_matches('/').to_string();
|
|
let (code, body) = match cur["status"].as_u64() {
|
|
Some(c) if c != 0 => (c as u16, Vec::new()),
|
|
_ if path == loki_rs::strata::SRC_ASSET => (200, src_bytes(&cur)),
|
|
_ if path == loki_rs::strata::ENGINE_ASSET => {
|
|
(200, s(&cur["engine"]).as_bytes().to_vec())
|
|
}
|
|
_ => (404, Vec::new()),
|
|
};
|
|
let mut r = Response::new(Full::new(Bytes::from(body)));
|
|
*r.status_mut() = hyper::StatusCode::from_u16(code).unwrap();
|
|
Ok::<_, Infallible>(r)
|
|
}
|
|
});
|
|
let _ = hyper::server::conn::http1::Builder::new()
|
|
.serve_connection(TokioIo::new(st), svc)
|
|
.await;
|
|
});
|
|
}
|
|
});
|
|
format!("http://{addr}")
|
|
}
|
|
|
|
fn src_bytes(rel: &Value) -> Vec<u8> {
|
|
tar_gz(rel["src"].as_array().map_or(&[][..], Vec::as_slice))
|
|
}
|
|
|
|
fn sha(b: &[u8]) -> String {
|
|
hex::encode(sha2::Sha256::digest(b))
|
|
}
|
|
|
|
struct Bench {
|
|
facade: SocketAddr,
|
|
go: Arc<fakego::FakeGo>,
|
|
_stop: tokio::sync::oneshot::Sender<()>,
|
|
home: PathBuf,
|
|
tools: PathBuf,
|
|
root: PathBuf,
|
|
strata: Arc<Strata>,
|
|
rel: Arc<Mutex<Value>>,
|
|
rel_base: String,
|
|
_tmp: Vec<tempfile::TempDir>,
|
|
}
|
|
|
|
async fn bench() -> Bench {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let tools = tempfile::tempdir().unwrap();
|
|
let root = tempfile::tempdir().unwrap();
|
|
let home = tmp.path().to_path_buf();
|
|
let probe = || Probe {
|
|
path: Some(vec![tools.path().to_path_buf()]),
|
|
root: root.path().to_path_buf(),
|
|
};
|
|
std::fs::create_dir_all(root.path().join("proc")).unwrap();
|
|
let rel = Arc::new(Mutex::new(json!({})));
|
|
let rel_base = fake_release(rel.clone()).await;
|
|
let b = Arc::new(Backends::with(
|
|
home.clone(),
|
|
String::new(),
|
|
probe(),
|
|
String::new(),
|
|
));
|
|
let mut st = Strata::new(home.clone(), probe());
|
|
st.release_base = format!("{rel_base}/");
|
|
st.platform_ok = true;
|
|
let strata = Arc::new(st);
|
|
let (facade, go, stop, _) = fakego::start_with(
|
|
home.clone(),
|
|
fakego::Opts {
|
|
backends: Some(b),
|
|
strata: Some(strata.clone()),
|
|
..Default::default()
|
|
},
|
|
)
|
|
.await;
|
|
*go.apply_presets.lock().unwrap() = true;
|
|
Bench {
|
|
facade,
|
|
go,
|
|
_stop: stop,
|
|
home,
|
|
tools: tools.path().to_path_buf(),
|
|
root: root.path().to_path_buf(),
|
|
strata,
|
|
rel,
|
|
rel_base,
|
|
_tmp: vec![tmp, tools, root],
|
|
}
|
|
}
|
|
|
|
impl Bench {
|
|
fn mask(&self, v: &str) -> String {
|
|
let ts = Regex::new(r#""(started_at|ended_at)":[1-9][0-9]*"#).unwrap();
|
|
let v = v
|
|
.replace(&self.rel_base, "{{REL}}")
|
|
.replace(&self.home.to_string_lossy().into_owned(), "{{HOME}}");
|
|
ts.replace_all(&v, r#""$1":"{{T}}""#).into_owned()
|
|
}
|
|
|
|
fn unmask(&self, v: &str) -> String {
|
|
v.replace("{{HOME}}", &self.home.to_string_lossy())
|
|
}
|
|
|
|
async fn call(&self, method: &str, path: &str, body: &str) -> (u16, Value) {
|
|
let (code, b) = fakego::send(self.facade, method, path, &[], body).await;
|
|
let out = serde_json::from_str(&self.mask(&String::from_utf8_lossy(&b)))
|
|
.unwrap_or_else(|_| panic!("{path} : corps illisible {b:?}"));
|
|
(code.as_u16(), out)
|
|
}
|
|
|
|
async fn wait_job(&self) {
|
|
for _ in 0..1500 {
|
|
let (_, v) = self.call("GET", "/api/llamacpp/job", "").await;
|
|
if v["running"] != json!(true) {
|
|
return;
|
|
}
|
|
tokio::time::sleep(Duration::from_millis(20)).await;
|
|
}
|
|
panic!("le job ne finit pas");
|
|
}
|
|
|
|
/// Les outils d'une machine (bash, mkdir et tr toujours).
|
|
fn set_machine(&self, machines: &Value, name: &str) {
|
|
for e in std::fs::read_dir(&self.tools).unwrap().flatten() {
|
|
std::fs::remove_file(e.path()).unwrap();
|
|
}
|
|
for tool in ["bash", "mkdir", "tr"] {
|
|
let p = loki_rs::paths::look_path(tool).expect(tool);
|
|
std::os::unix::fs::symlink(p, self.tools.join(tool)).unwrap();
|
|
}
|
|
for (tool, script) in machines[name].as_object().into_iter().flatten() {
|
|
write_file(&self.tools.join(tool), s(script), 0o755);
|
|
}
|
|
}
|
|
|
|
fn set_release(&self, r: &Value) {
|
|
*self.rel.lock().unwrap() = r.clone();
|
|
let mut want = std::collections::HashMap::from([
|
|
(loki_rs::strata::SRC_ASSET.to_string(), sha(&src_bytes(r))),
|
|
(
|
|
loki_rs::strata::ENGINE_ASSET.to_string(),
|
|
sha(s(&r["engine"]).as_bytes()),
|
|
),
|
|
]);
|
|
if r["bad_sha"] == json!(true) {
|
|
want.insert(loki_rs::strata::ENGINE_ASSET.to_string(), "0".repeat(64));
|
|
}
|
|
*self.strata.asset_sha.lock().unwrap() = want;
|
|
}
|
|
|
|
/// presets/ et strata/, sans le contenu des archives.
|
|
fn tree(&self) -> Vec<String> {
|
|
use std::os::unix::fs::PermissionsExt;
|
|
fn walk(b: &Bench, p: &Path, out: &mut Vec<String>) {
|
|
let Ok(m) = std::fs::symlink_metadata(p) else {
|
|
return;
|
|
};
|
|
let rel = p
|
|
.strip_prefix(&b.home)
|
|
.unwrap()
|
|
.to_string_lossy()
|
|
.into_owned();
|
|
if m.is_dir() {
|
|
out.push(format!("d {rel}"));
|
|
for e in std::fs::read_dir(p).unwrap().flatten() {
|
|
walk(b, &e.path(), out);
|
|
}
|
|
} else if rel.starts_with("strata/dl/") || rel.starts_with("strata/prebuilt/") {
|
|
out.push(format!("f {rel}"));
|
|
} else {
|
|
let c = std::fs::read(p).unwrap();
|
|
out.push(format!(
|
|
"f {rel} {} {}",
|
|
common::parity::perm_string(m.permissions().mode() & 0o777),
|
|
b.mask(&String::from_utf8_lossy(&c))
|
|
));
|
|
}
|
|
}
|
|
let mut out = Vec::new();
|
|
for root in ["presets", "strata"] {
|
|
walk(self, &self.home.join(root), &mut out);
|
|
}
|
|
out.sort();
|
|
out
|
|
}
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
|
async fn parite_de_strata() {
|
|
if !cfg!(target_arch = "x86_64") {
|
|
eprintln!("Strata n'est proposé que sous Linux x86_64 : rejeu sauté");
|
|
return;
|
|
}
|
|
let fx = fixture();
|
|
let bn = bench().await;
|
|
let machines = &fx["machines"];
|
|
for st in fx["steps"].as_array().unwrap() {
|
|
let name = s(&st["name"]);
|
|
if let Some(m) = st["machine"].as_str() {
|
|
bn.set_machine(machines, m);
|
|
}
|
|
if let Some(m) = st["meminfo"].as_str() {
|
|
std::fs::write(bn.root.join("proc/meminfo"), m).unwrap();
|
|
}
|
|
if let Some(d) = st["disk_free"].as_i64() {
|
|
*bn.strata.disk_free.lock().unwrap() = Some(d);
|
|
}
|
|
let files: serde_json::Map<String, Value> = st["files"]
|
|
.as_object()
|
|
.into_iter()
|
|
.flatten()
|
|
.map(|(k, v)| (k.clone(), Value::String(bn.unmask(s(v)))))
|
|
.collect();
|
|
apply_files(&bn.home, &Value::Object(files));
|
|
for (k, v) in st["config"].as_object().into_iter().flatten() {
|
|
let v = bn.unmask(s(v));
|
|
if v.is_empty() {
|
|
bn.go.del("config", k);
|
|
} else {
|
|
bn.go.set("config", k, &v);
|
|
}
|
|
}
|
|
for (k, v) in st["state"].as_object().into_iter().flatten() {
|
|
if s(v).is_empty() {
|
|
bn.go.del("state", k);
|
|
} else {
|
|
bn.go.set("state", k, s(v));
|
|
}
|
|
}
|
|
if !st["release"].is_null() {
|
|
bn.set_release(&st["release"]);
|
|
}
|
|
bn.go.preset_applies.lock().unwrap().clear();
|
|
*bn.go.tune_busy.lock().unwrap() =
|
|
(st["tune"].as_bool() == Some(true)).then(|| TUNE_BUSY.to_string());
|
|
let (code, out) = bn
|
|
.call(s(&st["method"]), s(&st["path"]), &bn.unmask(s(&st["body"])))
|
|
.await;
|
|
*bn.go.tune_busy.lock().unwrap() = None;
|
|
assert_eq!(code as i64, st["status"].as_i64().unwrap(), "{name}");
|
|
assert_eq!(out, st["out"], "{name}");
|
|
if st["wait"].as_bool() == Some(true) {
|
|
bn.wait_job().await;
|
|
let (_, job) = bn.call("GET", "/api/llamacpp/job?from=0", "").await;
|
|
assert_eq!(job, st["job"], "{name} : job");
|
|
}
|
|
// Une bascule faite par le Go : application du preset puis
|
|
// redémarrage du service.
|
|
let actions: Vec<Value> = bn
|
|
.go
|
|
.preset_applies
|
|
.lock()
|
|
.unwrap()
|
|
.iter()
|
|
.filter(|(_, mode)| mode == "switch")
|
|
.map(|_| json!("restart"))
|
|
.collect();
|
|
assert_eq!(Value::Array(actions), st["actions"], "{name} : service");
|
|
let mut after = BTreeMap::new();
|
|
for k in [
|
|
"ENGINE",
|
|
"STRATA_CONFIG",
|
|
"CTX",
|
|
"STRATA_KV",
|
|
"STRATA_HELPER",
|
|
] {
|
|
if let Some(v) = bn.go.get("config", k).filter(|v| !v.is_empty()) {
|
|
after.insert(format!("config/{k}"), Value::String(bn.mask(&v)));
|
|
}
|
|
}
|
|
assert_eq!(
|
|
Value::Object(after.into_iter().collect()),
|
|
st["after"],
|
|
"{name} : après"
|
|
);
|
|
let t: Vec<Value> = bn.tree().into_iter().map(Value::String).collect();
|
|
let t = if t.is_empty() {
|
|
Value::Null
|
|
} else {
|
|
Value::Array(t)
|
|
};
|
|
assert_eq!(t, st["tree"], "{name} : arborescence");
|
|
}
|
|
}
|
|
|
|
/// Sans les backends dans la façade (pas de job), Strata répond 503.
|
|
#[tokio::test]
|
|
async fn sans_backends() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let st = Strata::new(tmp.path().to_path_buf(), Probe::system());
|
|
let (facade, _go, _stop, _) = fakego::start_with(
|
|
tmp.path().to_path_buf(),
|
|
fakego::Opts {
|
|
strata: Some(Arc::new(st)),
|
|
..Default::default()
|
|
},
|
|
)
|
|
.await;
|
|
let (c, b) = fakego::send(facade, "GET", "/api/strata", &[], "").await;
|
|
assert_eq!(c.as_u16(), 503, "{b:?}");
|
|
}
|