mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-18 12:46:56 +02:00
Lot 28 : optimiseur tenu par la façade (LOKI_TUNE=rust)
Avec LOKI_TUNE=rust (expérimental, Unix seulement), la façade sert /api/tune, /api/tune/status, /api/tune/cancel, /api/tune/apply et /api/tune/last, et déroule l'optimisation sans perte (rust/loki-rs/src/tune) : vrai moteur arrêté, essais composés à blanc puis lancés par les « loki serve » du Go (LOKI_TUNE_TRIAL), chacun dans son groupe de processus, mesurés par le bench complet à profondeur fixe, retenus au-delà du bruit ; vrai moteur relancé. Rien n'est écrit sans un clic : copie du preset, ou le preset lui-même — redémarrage, sonde, et ancienne version rétablie si elle échoue. - tune/core.rs : la partie pure (EXTRA_ARGS, réglages, essais de chaque étape, garde-fous, diff et réécriture du preset, score) ; - tune/lock.rs : le verrou tune.lock au format du Go, identité des processus comme procIdentity (la reprise du Go reconnaît la façade vivante, et écarte son verrou si elle meurt), ménage d'un verrou périmé (essai orphelin arrêté, application interrompue défaite) ; - tune/job.rs : routes, job, essais et application. Le verrou de génération reste celui du Go, pris par le bail du moteur au motif « tune » (refus de l'optimiseur pour les autres, stop du chat qui annule). Le Go prête /internal/v1/tune/context (bench, génération, jobs du mode code, état du moteur, télémétrie) et le démarrage ou l'arrêt du moteur sans le refus de l'optimiseur (service/action « os »). Hors Unix, la façade passe LOKI_TUNE vide au Go, qui garde l'optimiseur. serde_json lit désormais les flottants à l'arrondi exact (float_roundtrip), comme le Go. Parité : parity_tune_test.go tire du Go un journal (partie pure, contenu du verrou, résultat enregistré, vérification après application) rejoué par tests/tune_core.rs. tests/tune.rs déroule de vraies optimisations, le binaire de test jouant « loki serve » ; les tests du verrou du Go sont portés ; TestFacadeTune côté Go. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
This commit is contained in:
31 files changed
+36838
-25
No files matched your search
@@ -67,6 +67,8 @@ pub struct Ctx {
|
||||
pub tasks: Option<std::sync::Arc<crate::tasks::Tasks>>,
|
||||
/// Banc d'essai tenu par la façade (`LOKI_BENCH=rust`).
|
||||
pub bench: Option<std::sync::Arc<crate::bench::job::Bench>>,
|
||||
/// Optimiseur tenu par la façade (`LOKI_TUNE=rust`).
|
||||
pub tune: Option<std::sync::Arc<crate::tune::job::Tune>>,
|
||||
/// Jeton de l'API interne : une requête qui le porte vient du Go.
|
||||
pub internal_token: String,
|
||||
}
|
||||
@@ -93,6 +95,7 @@ impl Ctx {
|
||||
|| self.strata.is_some()
|
||||
|| self.tasks.is_some()
|
||||
|| self.bench.is_some()
|
||||
|| self.tune.is_some()
|
||||
}
|
||||
|
||||
/// La requête vient du Go (jeton interne valide) : déjà authentifiée par
|
||||
|
||||
@@ -481,7 +481,7 @@ async fn load_last(ctx: &Ctx) -> Option<Saved> {
|
||||
|
||||
/// Le modèle du preset est-il hybride (reprise du cache aux points de
|
||||
/// contrôle) ?
|
||||
async fn model_hybrid(ctx: &Ctx, cfg: &Cfg) -> bool {
|
||||
pub(crate) async fn model_hybrid(ctx: &Ctx, cfg: &Cfg) -> bool {
|
||||
let m = cfg.get("MODEL").map(|s| s.trim()).unwrap_or("");
|
||||
if m.is_empty() {
|
||||
return false;
|
||||
|
||||
@@ -44,10 +44,10 @@ const DEPTH_MAX: i64 = 32768;
|
||||
const DEPTH_CPU: i64 = 16384;
|
||||
const DEPTH_MIN: i64 = 4096;
|
||||
const MARGIN: i64 = 512;
|
||||
const WARM_GEN: i64 = 32;
|
||||
pub(crate) const WARM_GEN: i64 = 32;
|
||||
const SEED: i64 = 1234;
|
||||
const REUSE_SLACK: i64 = 16;
|
||||
const SAMPLE_BYTES: usize = 160 << 10;
|
||||
pub(crate) const SAMPLE_BYTES: usize = 160 << 10;
|
||||
const FALLBACK_RATIO: f64 = 2.5;
|
||||
const THRASH_BYTES: i64 = 512 << 20;
|
||||
|
||||
@@ -62,7 +62,7 @@ pub const ERR_NO_TIMINGS: &str =
|
||||
pub const CORPUS: &str = include_str!("corpus.txt");
|
||||
|
||||
const ASK_PROSE: &str = "\n\nContinue this passage with another 1000+ words of original varied prose, mixing French and English narrative paragraphs on different topics.";
|
||||
const ASK_FIRST: &str = "Résume en quelques phrases le rôle de ce fichier.";
|
||||
pub(crate) const ASK_FIRST: &str = "Résume en quelques phrases le rôle de ce fichier.";
|
||||
const ASK_NEXT: &str =
|
||||
"Écris en JavaScript une fonction courte inspirée de cette partie, sans explication.";
|
||||
|
||||
|
||||
@@ -373,7 +373,7 @@ pub(crate) async fn generating(ctx: &Ctx) -> bool {
|
||||
|
||||
/// `asrShutdownVite` : la dictée de la façade si elle la tient, sinon celle
|
||||
/// du Go.
|
||||
async fn dictate_stop(ctx: &Ctx) {
|
||||
pub(crate) async fn dictate_stop(ctx: &Ctx) {
|
||||
if let Some(d) = &ctx.dictate {
|
||||
d.shutdown_fast().await;
|
||||
return;
|
||||
|
||||
@@ -47,6 +47,7 @@ pub mod system;
|
||||
pub mod sysupdate;
|
||||
pub mod tasks;
|
||||
pub mod tools;
|
||||
pub mod tune;
|
||||
pub mod tz;
|
||||
pub mod ui;
|
||||
pub mod update;
|
||||
@@ -186,6 +186,13 @@ fn serve() -> ExitCode {
|
||||
// LOKI_BENCH=rust : le banc d'essai (routes, job, protocole) ; le
|
||||
// verrou de génération reste celui du Go, pris par le bail du moteur.
|
||||
let bench_rust = held_by_facade("LOKI_BENCH");
|
||||
// LOKI_TUNE=rust : l'optimiseur (routes, job, essais, application).
|
||||
// Sous Unix seulement : l'identité des processus que porte son verrou
|
||||
// (procIdentity du Go) n'est pas portée ailleurs.
|
||||
let tune_rust = held_by_facade("LOKI_TUNE") && cfg!(unix);
|
||||
if held_by_facade("LOKI_TUNE") && !tune_rust {
|
||||
eprintln!("[loki-rs] optimiseur tenu par le Go (Unix seulement)");
|
||||
}
|
||||
let engine_rust = held_by_facade("LOKI_ENGINE")
|
||||
&& match loki_rs::enginectl::usable(
|
||||
std::env::var("LOKI_CONTAINER").ok().as_deref(),
|
||||
@@ -222,12 +229,17 @@ fn serve() -> ExitCode {
|
||||
("LOKI_STRATA", strata_rust),
|
||||
("LOKI_TASKS", tasks_rust),
|
||||
("LOKI_BENCH", bench_rust),
|
||||
("LOKI_TUNE", tune_rust),
|
||||
];
|
||||
for (var, on) in held {
|
||||
if on {
|
||||
env.push((var.into(), "rust".into()));
|
||||
}
|
||||
}
|
||||
// Demandé mais pas tenu : le Go ne doit pas relayer ces routes.
|
||||
if !tune_rust && held_by_facade("LOKI_TUNE") {
|
||||
env.push(("LOKI_TUNE".into(), String::new()));
|
||||
}
|
||||
if held.iter().any(|(_, on)| *on) {
|
||||
env.push(("LOKI_FACADE_ADDR".into(), format!("127.0.0.1:{web_port}")));
|
||||
}
|
||||
@@ -357,6 +369,12 @@ fn serve() -> ExitCode {
|
||||
.then(|| std::sync::Arc::new(loki_rs::tasks::Tasks::new(engine_paths.home.clone())));
|
||||
let bench = bench_rust
|
||||
.then(|| std::sync::Arc::new(loki_rs::bench::job::Bench::new(engine_paths.clone())));
|
||||
let tune = tune_rust.then(|| {
|
||||
std::sync::Arc::new(loki_rs::tune::job::Tune::new(
|
||||
engine_paths.home.clone(),
|
||||
go_bin_path.clone(),
|
||||
))
|
||||
});
|
||||
let cfg = proxy::Config {
|
||||
backend,
|
||||
internal_token: token,
|
||||
@@ -380,6 +398,7 @@ fn serve() -> ExitCode {
|
||||
strata,
|
||||
tasks,
|
||||
bench,
|
||||
tune,
|
||||
mcp: mcp_rust.then(|| std::sync::Arc::new(loki_rs::mcp::Mcp::new(mem_home))),
|
||||
};
|
||||
let res = proxy::serve(listener, cfg, shutdown_signal()).await;
|
||||
|
||||
@@ -109,6 +109,7 @@ pub struct Config {
|
||||
/// Banc d'essai (routes, job, protocole de mesure) tenu par la façade
|
||||
/// (`LOKI_BENCH=rust`) ; `None` : au Go.
|
||||
pub bench: Option<Arc<crate::bench::job::Bench>>,
|
||||
pub tune: Option<Arc<crate::tune::job::Tune>>,
|
||||
}
|
||||
|
||||
/// Arrête la tâche de fond tenue à sa sortie de portée.
|
||||
@@ -164,6 +165,7 @@ pub async fn serve(
|
||||
strata: cfg.strata,
|
||||
tasks: cfg.tasks,
|
||||
bench: cfg.bench,
|
||||
tune: cfg.tune,
|
||||
internal_token: cfg.internal_token.clone(),
|
||||
},
|
||||
});
|
||||
@@ -296,6 +298,11 @@ impl Proxy {
|
||||
{
|
||||
return crate::bench::job::serve(&self.ctx, b, req, from_go).await;
|
||||
}
|
||||
if let Some(t) = &self.ctx.tune
|
||||
&& crate::tune::job::handles(path)
|
||||
{
|
||||
return crate::tune::job::serve(&self.ctx, t, req, from_go).await;
|
||||
}
|
||||
if let Some(b) = &self.ctx.backends
|
||||
&& crate::backends::handles(path)
|
||||
{
|
||||
|
||||
@@ -219,7 +219,7 @@ fn ceil_quota(quota: &str, period: &str) -> i64 {
|
||||
}
|
||||
}
|
||||
|
||||
fn read_cpu_list(root: &Path, rel: &str) -> Option<Vec<i64>> {
|
||||
pub(crate) fn read_cpu_list(root: &Path, rel: &str) -> Option<Vec<i64>> {
|
||||
parse_cpu_list(&read(root, rel)?)
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,615 @@
|
||||
//! Verrou de l'optimiseur — port de `backend_tune_lock.go`, au même format.
|
||||
//!
|
||||
//! Pendant une optimisation, la VRAM appartient au moteur d'ESSAI : relancer
|
||||
//! le vrai moteur à côté chargerait deux modèles sur des cartes qui n'en
|
||||
//! tiennent qu'un. Le verrou est un FICHIER de LOKI_HOME, créé de façon
|
||||
//! exclusive, que tout le monde lit : le Go (`tuneGuard`, tous ses refus), un
|
||||
//! « loki tune » en ligne de commande, et cette façade.
|
||||
//!
|
||||
//! Le fichier porte l'identité du processus qui le tient (PID, instant de
|
||||
//! démarrage, binaire) et celle de l'essai en cours. Un verrou dont le
|
||||
//! propriétaire est mort est périmé : l'essai orphelin est arrêté — seulement
|
||||
//! si PID, instant de démarrage ET binaire concordent — puis le fichier est
|
||||
//! retiré. L'identité est écrite comme le Go l'écrit (`procIdentity`) : la
|
||||
//! reprise du Go (`tuneRecoverWatch`, toutes les 30 s) doit reconnaître la
|
||||
//! façade vivante, sans quoi elle écarterait son verrou en pleine mesure.
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Mutex;
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// `tuneLockName`.
|
||||
pub const LOCK_NAME: &str = "tune.lock";
|
||||
|
||||
/// `tuneLockFreshness` : un verrou illisible plus jeune que ça est en cours
|
||||
/// d'écriture, pas abandonné.
|
||||
pub const FRESHNESS: Duration = Duration::from_secs(10);
|
||||
|
||||
/// `errTuneBusy`.
|
||||
pub const ERR_TUNE_BUSY: &str = "optimisation en cours (loki tune) : le moteur est réservé aux essais — attends la fin ou annule-la";
|
||||
|
||||
/// `tuneProc` : un processus désigné sans ambiguïté.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Proc {
|
||||
pub pid: i64,
|
||||
/// `procIdentity` ; vide = illisible au moment de l'écrire.
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
pub start: String,
|
||||
/// Binaires admis (un « loki serve » devient llama-server par exec).
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub exes: Vec<String>,
|
||||
}
|
||||
|
||||
/// `tuneOwner` : le contenu du verrou.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Owner {
|
||||
pub owner: Proc,
|
||||
/// « web », « cli » ou « apply ».
|
||||
#[serde(default)]
|
||||
pub via: String,
|
||||
/// « essais », « application », « relance », « rendu ».
|
||||
#[serde(default)]
|
||||
pub phase: String,
|
||||
#[serde(default)]
|
||||
pub since: i64,
|
||||
/// Distingue deux verrous pris par le même processus dans la même seconde.
|
||||
#[serde(default, skip_serializing_if = "is_zero")]
|
||||
pub nonce: i64,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub trial: Option<Proc>,
|
||||
/// Le vrai moteur tournait avant l'optimisation.
|
||||
#[serde(default)]
|
||||
pub main_was_active: bool,
|
||||
/// Application en cours : la version d'avant du preset, sauvée avant
|
||||
/// toute écriture.
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
pub backup: String,
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
pub preset_id: String,
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
pub preset_name: String,
|
||||
}
|
||||
|
||||
fn is_zero(v: &i64) -> bool {
|
||||
*v == 0
|
||||
}
|
||||
|
||||
/// `tuneHeldKey`.
|
||||
pub fn held_key(o: &Owner) -> String {
|
||||
format!("{}|{}|{}", o.via, o.since, o.nonce)
|
||||
}
|
||||
|
||||
/// `sameTuneLock` : la même prise de verrou, quelle que soit sa phase.
|
||||
pub fn same_lock(a: &Owner, b: &Owner) -> bool {
|
||||
a.owner.pid == b.owner.pid && a.owner.start == b.owner.start && held_key(a) == held_key(b)
|
||||
}
|
||||
|
||||
/// `sameExe` : deux binaires comparés par leur nom de fichier (sans .exe,
|
||||
/// sans casse).
|
||||
pub fn same_exe(a: &str, b: &str) -> bool {
|
||||
let norm = |s: &str| {
|
||||
let s = s.trim();
|
||||
let s = s.strip_suffix(" (deleted)").unwrap_or(s);
|
||||
let s = s.replace('\\', "/");
|
||||
let base = go_base(&s).to_lowercase();
|
||||
base.strip_suffix(".exe").unwrap_or(&base).to_string()
|
||||
};
|
||||
!a.is_empty() && !b.is_empty() && norm(a) == norm(b)
|
||||
}
|
||||
|
||||
/// `filepath.Base` (séparateur `/`).
|
||||
fn go_base(p: &str) -> String {
|
||||
let t = p.trim_end_matches('/');
|
||||
if t.is_empty() {
|
||||
return if p.is_empty() { ".".into() } else { "/".into() };
|
||||
}
|
||||
t.rsplit('/').next().unwrap_or(t).to_string()
|
||||
}
|
||||
|
||||
/// `readTuneOwner` : illisible, ou sans PID : pas de verrou.
|
||||
pub fn read_owner(path: &Path) -> Option<Owner> {
|
||||
let b = std::fs::read(path).ok()?;
|
||||
let o: Owner = serde_json::from_slice(&b).ok()?;
|
||||
(o.owner.pid > 0).then_some(o)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Identité d'un processus
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// `procIdentity` (Linux) : instant de démarrage (champ 22 de
|
||||
/// `/proc/<pid>/stat`, préfixé de l'identifiant du démarrage de la machine)
|
||||
/// et binaire. Un zombie a fini de tourner : il ne compte plus.
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn proc_identity(pid: i64) -> Option<(String, String)> {
|
||||
proc_identity_in(Path::new("/"), pid)
|
||||
}
|
||||
|
||||
/// [`proc_identity`] sous la racine `root` (les tests).
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn proc_identity_in(root: &Path, pid: i64) -> Option<(String, String)> {
|
||||
if pid <= 0 {
|
||||
return None;
|
||||
}
|
||||
let s = std::fs::read_to_string(root.join(format!("proc/{pid}/stat"))).ok()?;
|
||||
let st = crate::engine::proctree::parse_proc_stat(&s)?;
|
||||
if st.state == "Z" {
|
||||
return None;
|
||||
}
|
||||
let exe = std::fs::read_link(root.join(format!("proc/{pid}/exe")))
|
||||
.map(|p| p.to_string_lossy().into_owned())
|
||||
.unwrap_or_default();
|
||||
// Binaire remplacé par une mise à jour pendant que le processus tourne :
|
||||
// le lien porte « (deleted) », c'est pourtant bien le même processus.
|
||||
let exe = exe.strip_suffix(" (deleted)").unwrap_or(&exe).to_string();
|
||||
let mut start = st.start;
|
||||
if let Ok(b) = std::fs::read_to_string(root.join("proc/sys/kernel/random/boot_id")) {
|
||||
let id = b.trim();
|
||||
if !id.is_empty() {
|
||||
start = format!("{id}/{start}");
|
||||
}
|
||||
}
|
||||
Some((start, exe))
|
||||
}
|
||||
|
||||
/// `procIdentity` (autres Unix) : `ps`, langue et fuseau fixés — le
|
||||
/// processus web et un terminal en français doivent lire la même chaîne.
|
||||
#[cfg(all(unix, not(target_os = "linux")))]
|
||||
pub fn proc_identity(pid: i64) -> Option<(String, String)> {
|
||||
if pid <= 0 || !pid_alive(pid) {
|
||||
return None;
|
||||
}
|
||||
let p = pid.to_string();
|
||||
let out = std::process::Command::new("ps")
|
||||
.args(["-o", "lstart=", "-p", &p])
|
||||
.env("LC_ALL", "C")
|
||||
.env("LANG", "C")
|
||||
.env("TZ", "UTC")
|
||||
.output()
|
||||
.ok()?;
|
||||
if !out.status.success() {
|
||||
return None;
|
||||
}
|
||||
let start = String::from_utf8_lossy(&out.stdout)
|
||||
.split_whitespace()
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
if start.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let exe = std::process::Command::new("ps")
|
||||
.args(["-o", "comm=", "-p", &p])
|
||||
.output()
|
||||
.ok()
|
||||
.filter(|o| o.status.success())
|
||||
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
|
||||
.unwrap_or_default();
|
||||
Some((start, exe))
|
||||
}
|
||||
|
||||
/// Hors Unix, la façade ne tient pas l'optimiseur (`main.rs`).
|
||||
#[cfg(not(unix))]
|
||||
pub fn proc_identity(_pid: i64) -> Option<(String, String)> {
|
||||
None
|
||||
}
|
||||
|
||||
/// `processAlive` : `kill(pid, 0)`.
|
||||
#[cfg(unix)]
|
||||
pub fn pid_alive(pid: i64) -> bool {
|
||||
let Ok(p) = i32::try_from(pid) else {
|
||||
return false;
|
||||
};
|
||||
// SAFETY: kill(2) au signal 0 n'envoie rien ; un PID absent rend -1.
|
||||
p > 0 && unsafe { libc::kill(p, 0) == 0 }
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
pub fn pid_alive(_pid: i64) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// `tuneProcAlive` : `p` désigne-t-il encore le MÊME processus ?
|
||||
pub fn proc_alive(p: &Proc) -> bool {
|
||||
let Some((start, exe)) = proc_identity(p.pid) else {
|
||||
return false;
|
||||
};
|
||||
if !p.start.is_empty() && start != p.start {
|
||||
return false;
|
||||
}
|
||||
if exe.is_empty() || p.exes.is_empty() {
|
||||
return true;
|
||||
}
|
||||
p.exes.iter().any(|e| same_exe(e, &exe))
|
||||
}
|
||||
|
||||
/// `selfProc` : ce processus, tel que le verrou le note.
|
||||
pub fn self_proc() -> Proc {
|
||||
let pid = i64::from(std::process::id());
|
||||
let mut p = Proc {
|
||||
pid,
|
||||
..Default::default()
|
||||
};
|
||||
if let Some((start, exe)) = proc_identity(pid) {
|
||||
p.start = start;
|
||||
if !exe.is_empty() {
|
||||
p.exes = vec![exe];
|
||||
}
|
||||
}
|
||||
if p.exes.is_empty()
|
||||
&& let Ok(me) = std::env::current_exe()
|
||||
{
|
||||
p.exes = vec![me.to_string_lossy().into_owned()];
|
||||
}
|
||||
p
|
||||
}
|
||||
|
||||
/// `tuneKillTree` (Unix) : SIGTERM au groupe de l'essai, puis SIGKILL s'il
|
||||
/// traîne. `gone` dit si le processus a disparu (récolté par l'appelant, ou
|
||||
/// sondé pour un orphelin d'un ancien Loki).
|
||||
#[cfg(unix)]
|
||||
pub fn kill_tree(pid: i64, gone: &dyn Fn() -> bool) {
|
||||
let Ok(p) = i32::try_from(pid) else {
|
||||
return;
|
||||
};
|
||||
if p <= 0 {
|
||||
return;
|
||||
}
|
||||
let wait = |d: Duration| {
|
||||
let deadline = std::time::Instant::now() + d;
|
||||
while std::time::Instant::now() < deadline {
|
||||
if gone() {
|
||||
return true;
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
}
|
||||
false
|
||||
};
|
||||
// SAFETY: kill(2) n'a pas d'effet mémoire.
|
||||
unsafe {
|
||||
if libc::kill(-p, libc::SIGTERM) != 0 {
|
||||
libc::kill(p, libc::SIGTERM);
|
||||
}
|
||||
}
|
||||
if wait(Duration::from_secs(10)) {
|
||||
return;
|
||||
}
|
||||
// SAFETY: idem.
|
||||
unsafe {
|
||||
libc::kill(-p, libc::SIGKILL);
|
||||
}
|
||||
wait(Duration::from_secs(5));
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
pub fn kill_tree(_pid: i64, _gone: &dyn Fn() -> bool) {}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Le verrou
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Qui est vivant : [`proc_alive`] hors tests.
|
||||
pub type AliveFn = Box<dyn Fn(&Proc) -> bool + Send + Sync>;
|
||||
|
||||
/// Le fichier de verrou de CE processus : les prises en cours (`tuneHeld`)
|
||||
/// et le verrou des opérations sur le fichier (`tuneFileMu`).
|
||||
pub struct LockFile {
|
||||
pub path: PathBuf,
|
||||
held: Mutex<HashSet<String>>,
|
||||
file_mu: Mutex<()>,
|
||||
alive: AliveFn,
|
||||
pid: i64,
|
||||
me: Proc,
|
||||
}
|
||||
|
||||
/// Une prise de verrou à défaire après le ménage d'un verrou périmé : l'essai
|
||||
/// orphelin à arrêter et l'application à défaire sont à l'appelant.
|
||||
pub type Claimed = Owner;
|
||||
|
||||
impl LockFile {
|
||||
pub fn new(home: &Path) -> Self {
|
||||
Self::with(home, Box::new(proc_alive), self_proc())
|
||||
}
|
||||
|
||||
/// `alive` et `me` remplaçables par les tests.
|
||||
pub fn with(home: &Path, alive: AliveFn, me: Proc) -> Self {
|
||||
LockFile {
|
||||
path: home.join(LOCK_NAME),
|
||||
held: Mutex::new(HashSet::new()),
|
||||
file_mu: Mutex::new(()),
|
||||
alive,
|
||||
pid: me.pid,
|
||||
me,
|
||||
}
|
||||
}
|
||||
|
||||
fn held(&self) -> std::sync::MutexGuard<'_, HashSet<String>> {
|
||||
self.held.lock().unwrap_or_else(|e| e.into_inner())
|
||||
}
|
||||
|
||||
fn file(&self) -> std::sync::MutexGuard<'_, ()> {
|
||||
self.file_mu.lock().unwrap_or_else(|e| e.into_inner())
|
||||
}
|
||||
|
||||
/// `tuneOwnerAlive` : un verrou qui porte NOTRE PID sans être l'un des
|
||||
/// nôtres vient d'un processus mort dont le PID nous est échu.
|
||||
pub fn owner_alive(&self, o: &Owner) -> bool {
|
||||
if o.owner.pid == self.pid && !self.held().contains(&held_key(o)) {
|
||||
return false;
|
||||
}
|
||||
(self.alive)(&o.owner)
|
||||
}
|
||||
|
||||
pub fn proc_alive(&self, p: &Proc) -> bool {
|
||||
(self.alive)(p)
|
||||
}
|
||||
|
||||
/// `tuneActive` : une optimisation tient-elle le moteur ? Un verrou
|
||||
/// périmé ne compte pas.
|
||||
pub fn active(&self) -> Option<Owner> {
|
||||
let o = read_owner(&self.path)?;
|
||||
self.owner_alive(&o).then_some(o)
|
||||
}
|
||||
|
||||
/// `tuneLockAcquire`, premier temps : la prise, ou qui la tient. `Err(None)`
|
||||
/// : un verrou périmé à écarter ([`Self::reap_claim`]) avant de réessayer.
|
||||
fn try_acquire(&self, owner: &Owner) -> Result<(), Option<String>> {
|
||||
let _f = self.file();
|
||||
let mut opts = std::fs::OpenOptions::new();
|
||||
opts.write(true).create_new(true);
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
opts.mode(0o644);
|
||||
}
|
||||
match opts.open(&self.path) {
|
||||
Ok(mut f) => {
|
||||
use std::io::Write as _;
|
||||
let b = serde_json::to_vec(owner).unwrap_or_default();
|
||||
let w = f.write_all(&b).and_then(|_| f.sync_all());
|
||||
drop(f);
|
||||
if let Err(e) = w {
|
||||
let _ = std::fs::remove_file(&self.path);
|
||||
return Err(Some(format!("verrou de l'optimiseur : {e}")));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => Err(None),
|
||||
Err(e) => Err(Some(e.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
/// `tuneLockAcquire` : prend le verrou, ou dit qui le tient. `reap` écarte
|
||||
/// un verrou périmé (essai orphelin et application interrompue compris).
|
||||
pub async fn acquire<F, Fut>(
|
||||
&self,
|
||||
via: &str,
|
||||
main_active: bool,
|
||||
reap: F,
|
||||
) -> Result<Held<'_>, String>
|
||||
where
|
||||
F: Fn() -> Fut,
|
||||
Fut: std::future::Future<Output = bool>,
|
||||
{
|
||||
if let Some(dir) = self.path.parent() {
|
||||
std::fs::create_dir_all(dir).map_err(|e| e.to_string())?;
|
||||
}
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default();
|
||||
let owner = Owner {
|
||||
owner: self.me.clone(),
|
||||
via: via.into(),
|
||||
phase: "essais".into(),
|
||||
since: now.as_secs() as i64,
|
||||
nonce: now.as_nanos() as i64,
|
||||
main_was_active: main_active,
|
||||
..Default::default()
|
||||
};
|
||||
// Inscrit comme nôtre AVANT que le fichier n'existe : la reprise tient
|
||||
// pour mort un verrou qui porte notre PID sans être inscrit.
|
||||
let key = held_key(&owner);
|
||||
self.held().insert(key.clone());
|
||||
for _ in 0..3 {
|
||||
match self.try_acquire(&owner) {
|
||||
Ok(()) => {
|
||||
return Ok(Held {
|
||||
file: self,
|
||||
owner: Mutex::new(owner),
|
||||
held: Mutex::new(true),
|
||||
});
|
||||
}
|
||||
Err(Some(e)) => {
|
||||
self.held().remove(&key);
|
||||
return Err(e);
|
||||
}
|
||||
Err(None) => {}
|
||||
}
|
||||
if let Some(o) = self.active() {
|
||||
self.held().remove(&key);
|
||||
let c = crate::tz::Location::local().civil(o.since);
|
||||
return Err(format!(
|
||||
"une optimisation tourne déjà ({}, PID {}, depuis {:02}:{:02})",
|
||||
o.via, o.owner.pid, c.hour, c.minute
|
||||
));
|
||||
}
|
||||
reap().await;
|
||||
}
|
||||
self.held().remove(&key);
|
||||
Err("verrou de l'optimiseur indisponible : réessaie".into())
|
||||
}
|
||||
|
||||
/// `tuneReapClaim` : la partie du ménage qui touche au fichier — lecture,
|
||||
/// déplacement, relecture, remise en place. Rend le verrou périmé
|
||||
/// effectivement retiré (`None` : rien à écarter).
|
||||
pub fn reap_claim(&self) -> Option<Claimed> {
|
||||
let _f = self.file();
|
||||
let Some(o) = read_owner(&self.path) else {
|
||||
// Illisible (écriture interrompue) : périmé lui aussi — mais pas
|
||||
// tout juste créé (un verrou neuf est vide quelques instants).
|
||||
if let Ok(md) = std::fs::metadata(&self.path)
|
||||
&& md
|
||||
.modified()
|
||||
.ok()
|
||||
.and_then(|m| m.elapsed().ok())
|
||||
.is_some_and(|age| age > FRESHNESS)
|
||||
{
|
||||
let _ = std::fs::remove_file(&self.path);
|
||||
}
|
||||
return None;
|
||||
};
|
||||
if self.owner_alive(&o) {
|
||||
return None;
|
||||
}
|
||||
let nanos = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_nanos();
|
||||
let stale = PathBuf::from(format!(
|
||||
"{}.stale-{}-{nanos}",
|
||||
self.path.display(),
|
||||
self.pid
|
||||
));
|
||||
std::fs::rename(&self.path, &stale).ok()?;
|
||||
let back = |stale: &Path| {
|
||||
// Un autre verrou, vivant, déplacé par erreur : remis en place (un
|
||||
// lien : jamais par-dessus un verrou posé entre-temps).
|
||||
if let Err(e) = std::fs::hard_link(stale, &self.path) {
|
||||
eprintln!(
|
||||
"[loki tune] verrou vivant déplacé par erreur, remise en place impossible : {e}"
|
||||
);
|
||||
}
|
||||
};
|
||||
let got = read_owner(&stale);
|
||||
let claimed = match got {
|
||||
Some(g) if same_lock(&g, &o) && !self.owner_alive(&g) => Some(o),
|
||||
_ => {
|
||||
back(&stale);
|
||||
None
|
||||
}
|
||||
};
|
||||
let _ = std::fs::remove_file(&stale);
|
||||
claimed
|
||||
}
|
||||
}
|
||||
|
||||
/// Le verrou tenu par CE processus (`tuneLock`).
|
||||
pub struct Held<'a> {
|
||||
file: &'a LockFile,
|
||||
owner: Mutex<Owner>,
|
||||
held: Mutex<bool>,
|
||||
}
|
||||
|
||||
impl Held<'_> {
|
||||
fn owner(&self) -> std::sync::MutexGuard<'_, Owner> {
|
||||
self.owner.lock().unwrap_or_else(|e| e.into_inner())
|
||||
}
|
||||
|
||||
pub fn snapshot(&self) -> Owner {
|
||||
self.owner().clone()
|
||||
}
|
||||
|
||||
/// `writeLocked` : réécrit d'un bloc (fichier voisin puis renommage) — un
|
||||
/// lecteur ne voit jamais un JSON à moitié écrit.
|
||||
fn write_locked(&self, o: &Owner) -> std::io::Result<()> {
|
||||
let b = serde_json::to_vec(o).map_err(std::io::Error::other)?;
|
||||
let tmp = PathBuf::from(format!(
|
||||
"{}.tmp-{}",
|
||||
self.file.path.display(),
|
||||
self.file.pid
|
||||
));
|
||||
std::fs::write(&tmp, b)?;
|
||||
let mut i = 0;
|
||||
loop {
|
||||
match std::fs::rename(&tmp, &self.file.path) {
|
||||
Ok(()) => return Ok(()),
|
||||
Err(e) if i == 4 => {
|
||||
let _ = std::fs::remove_file(&tmp);
|
||||
return Err(e);
|
||||
}
|
||||
Err(_) => {
|
||||
i += 1;
|
||||
std::thread::sleep(Duration::from_millis(50));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&self, f: impl FnOnce(&mut Owner)) {
|
||||
let mut o = self.owner();
|
||||
f(&mut o);
|
||||
let _f = self.file.file();
|
||||
let _ = self.write_locked(&o);
|
||||
}
|
||||
|
||||
/// `setTrial` : l'essai en cours (`None` : aucun), noté AVANT qu'il ne
|
||||
/// charge quoi que ce soit — c'est ce qui permet de l'arrêter après un
|
||||
/// redémarrage.
|
||||
pub fn set_trial(&self, p: Option<Proc>) {
|
||||
self.update(|o| o.trial = p);
|
||||
}
|
||||
|
||||
pub fn set_phase(&self, phase: &str) {
|
||||
self.update(|o| o.phase = phase.into());
|
||||
}
|
||||
|
||||
/// `setBackup` : la sauvegarde du preset avant son écriture.
|
||||
pub fn set_backup(&self, backup: &str, id: &str, name: &str) {
|
||||
self.update(|o| {
|
||||
o.backup = backup.into();
|
||||
o.preset_id = id.into();
|
||||
o.preset_name = name.into();
|
||||
});
|
||||
}
|
||||
|
||||
/// `release` : rend le verrou — seulement s'il est encore le nôtre. Réécrit
|
||||
/// d'abord NEUTRE (rien à défaire, rien à relancer, aucun essai), puis
|
||||
/// retiré ; l'inscription ne part qu'après.
|
||||
pub fn release(&self) {
|
||||
let mut o = self.owner();
|
||||
let _f = self.file.file();
|
||||
let mut held = self.held.lock().unwrap_or_else(|e| e.into_inner());
|
||||
if *held {
|
||||
let mut ours = false;
|
||||
for _ in 0..5 {
|
||||
if let Some(cur) = read_owner(&self.file.path) {
|
||||
ours = same_lock(&cur, &o);
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(20));
|
||||
}
|
||||
if ours {
|
||||
o.phase = "rendu".into();
|
||||
o.trial = None;
|
||||
o.main_was_active = false;
|
||||
o.backup.clear();
|
||||
o.preset_id.clear();
|
||||
o.preset_name.clear();
|
||||
let _ = self.write_locked(&o);
|
||||
for _ in 0..5 {
|
||||
match std::fs::remove_file(&self.file.path) {
|
||||
Ok(()) => break,
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => break,
|
||||
Err(_) => std::thread::sleep(Duration::from_millis(20)),
|
||||
}
|
||||
}
|
||||
}
|
||||
*held = false;
|
||||
self.file.held().remove(&held_key(&o));
|
||||
return;
|
||||
}
|
||||
if read_owner(&self.file.path).is_some_and(|cur| same_lock(&cur, &o)) {
|
||||
let _ = std::fs::remove_file(&self.file.path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Held<'_> {
|
||||
fn drop(&mut self) {
|
||||
if *self.held.lock().unwrap_or_else(|e| e.into_inner()) {
|
||||
self.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
//! Optimiseur sans perte tenu par la façade (lot 28 de la migration,
|
||||
//! `LOKI_TUNE=rust` ; expérimental, le Go reste le défaut) — port de
|
||||
//! `backend_tune*.go`.
|
||||
//!
|
||||
//! - [`core`] : la partie pure (essais, garde-fous, score, réécriture du
|
||||
//! preset) ;
|
||||
//! - [`lock`] : le verrou de LOKI_HOME, au format du Go ;
|
||||
//! - [`job`] : les routes, le job, les essais et l'application.
|
||||
|
||||
pub mod core;
|
||||
pub mod job;
|
||||
pub mod lock;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -0,0 +1,294 @@
|
||||
//! Tests de l'optimiseur : ceux du verrou (`backend_tune_test.go`), portés,
|
||||
//! et les aides du job.
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use super::lock::*;
|
||||
|
||||
fn me() -> Proc {
|
||||
Proc {
|
||||
pid: i64::from(std::process::id()),
|
||||
start: "moi".into(),
|
||||
exes: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// `TestTuneLock` : le verrou se prend, bloque, note l'essai, se rend ; un
|
||||
/// propriétaire mort le rend périmé et le ménage le retire.
|
||||
#[tokio::test]
|
||||
async fn verrou() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let alive = Arc::new(AtomicBool::new(true));
|
||||
let a = alive.clone();
|
||||
let pid = me().pid;
|
||||
let lf = LockFile::with(
|
||||
tmp.path(),
|
||||
Box::new(move |p: &Proc| a.load(Ordering::SeqCst) && p.pid == pid),
|
||||
me(),
|
||||
);
|
||||
let no_reap = || async { false };
|
||||
assert!(lf.active().is_none(), "garde levée sans verrou");
|
||||
let l = lf.acquire("cli", true, no_reap).await.unwrap();
|
||||
assert!(lf.active().is_some(), "garde muette pendant l'optimisation");
|
||||
let e = lf.acquire("web", false, no_reap).await.err().unwrap();
|
||||
assert!(
|
||||
e.starts_with(&format!(
|
||||
"une optimisation tourne déjà (cli, PID {pid}, depuis "
|
||||
)),
|
||||
"{e}"
|
||||
);
|
||||
l.set_trial(Some(Proc {
|
||||
pid: 999999,
|
||||
start: "x".into(),
|
||||
exes: vec![],
|
||||
}));
|
||||
let o = read_owner(&lf.path).unwrap();
|
||||
assert_eq!(o.trial.as_ref().map(|t| t.pid), Some(999999));
|
||||
assert!(o.main_was_active && o.phase == "essais" && o.via == "cli");
|
||||
l.set_phase("relance");
|
||||
l.set_backup("/b", "Dense", "Dense");
|
||||
let o = read_owner(&lf.path).unwrap();
|
||||
assert_eq!(
|
||||
(o.phase.as_str(), o.backup.as_str(), o.preset_id.as_str()),
|
||||
("relance", "/b", "Dense")
|
||||
);
|
||||
l.release();
|
||||
assert!(!lf.path.exists(), "verrou non rendu");
|
||||
|
||||
// Propriétaire mort : périmé, il ne bloque rien et cède la place.
|
||||
let l = lf.acquire("cli", true, no_reap).await.unwrap();
|
||||
l.set_trial(Some(Proc {
|
||||
pid: 999999,
|
||||
start: "x".into(),
|
||||
exes: vec![],
|
||||
}));
|
||||
alive.store(false, Ordering::SeqCst);
|
||||
assert!(lf.active().is_none(), "verrou périmé encore bloquant");
|
||||
let claimed = lf.reap_claim().expect("verrou périmé non écarté");
|
||||
assert!(claimed.main_was_active, "MainWasActive perdu au nettoyage");
|
||||
assert!(!lf.path.exists(), "verrou périmé non retiré");
|
||||
drop(l);
|
||||
alive.store(true, Ordering::SeqCst);
|
||||
let l2 = lf.acquire("web", false, no_reap).await.unwrap();
|
||||
// Un verrou rendu par sa poignée n'emporte pas celui d'un autre.
|
||||
let other = l2.snapshot();
|
||||
l2.release();
|
||||
std::fs::write(&lf.path, serde_json::to_vec(&other).unwrap()).unwrap();
|
||||
assert!(lf.path.exists());
|
||||
}
|
||||
|
||||
/// Un verrou périmé dans la place, et `acquire` qui le fait écarter.
|
||||
#[tokio::test]
|
||||
async fn verrou_perime_ecarte_a_la_prise() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let lf = LockFile::with(tmp.path(), Box::new(|_: &Proc| false), me());
|
||||
let dead = Owner {
|
||||
owner: Proc {
|
||||
pid: 999999,
|
||||
start: "x".into(),
|
||||
exes: vec![],
|
||||
},
|
||||
via: "cli".into(),
|
||||
phase: "essais".into(),
|
||||
main_was_active: true,
|
||||
..Default::default()
|
||||
};
|
||||
std::fs::write(&lf.path, serde_json::to_vec(&dead).unwrap()).unwrap();
|
||||
let reaped = std::sync::Mutex::new(0);
|
||||
let l = lf
|
||||
.acquire("web", false, || async {
|
||||
*reaped.lock().unwrap() += 1;
|
||||
lf.reap_claim().is_some()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(*reaped.lock().unwrap(), 1);
|
||||
assert_eq!(read_owner(&lf.path).unwrap().via, "web");
|
||||
drop(l);
|
||||
assert!(!lf.path.exists(), "verrou lâché sans release : rendu");
|
||||
}
|
||||
|
||||
/// `TestTuneLockStaleSelfAndFresh` : un verrou à notre PID qui n'est pas
|
||||
/// l'un des nôtres est périmé ; illisible et neuf il reste, vieux il part.
|
||||
#[tokio::test]
|
||||
async fn verrou_perime_de_soi_et_frais() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let lf = LockFile::with(tmp.path(), Box::new(|_: &Proc| true), me());
|
||||
let stale = Owner {
|
||||
owner: me(),
|
||||
via: "web".into(),
|
||||
phase: "essais".into(),
|
||||
..Default::default()
|
||||
};
|
||||
std::fs::write(&lf.path, serde_json::to_vec(&stale).unwrap()).unwrap();
|
||||
assert!(
|
||||
lf.active().is_none(),
|
||||
"verrou d'un ancien processus au même PID encore bloquant"
|
||||
);
|
||||
let l = lf
|
||||
.acquire("cli", false, || async { lf.reap_claim().is_some() })
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(lf.active().is_some(), "notre propre verrou ne bloque plus");
|
||||
let mine = l.snapshot();
|
||||
l.release();
|
||||
// Rendu : plus inscrit comme tenu, le même contenu est périmé.
|
||||
std::fs::write(&lf.path, serde_json::to_vec(&mine).unwrap()).unwrap();
|
||||
assert!(
|
||||
lf.active().is_none(),
|
||||
"verrou rendu encore inscrit comme tenu"
|
||||
);
|
||||
std::fs::remove_file(&lf.path).unwrap();
|
||||
|
||||
// Illisible et neuf : laissé en place.
|
||||
std::fs::write(&lf.path, b"").unwrap();
|
||||
assert!(lf.reap_claim().is_none());
|
||||
assert!(lf.path.exists(), "verrou en cours d'écriture retiré");
|
||||
// Illisible et vieux : écarté.
|
||||
let f = std::fs::File::options().write(true).open(&lf.path).unwrap();
|
||||
f.set_modified(SystemTime::now() - 2 * FRESHNESS).unwrap();
|
||||
drop(f);
|
||||
assert!(lf.reap_claim().is_none());
|
||||
assert!(!lf.path.exists(), "verrou illisible abandonné non retiré");
|
||||
}
|
||||
|
||||
/// Le ménage relit ce qu'il a déplacé : un verrou vivant pris entre la
|
||||
/// lecture et le déplacement est remis en place.
|
||||
#[tokio::test]
|
||||
async fn menage_jamais_sur_un_verrou_vivant() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let alive = Arc::new(AtomicBool::new(false));
|
||||
let a = alive.clone();
|
||||
let lf = LockFile::with(
|
||||
tmp.path(),
|
||||
Box::new(move |_: &Proc| a.load(Ordering::SeqCst)),
|
||||
me(),
|
||||
);
|
||||
let o = Owner {
|
||||
owner: Proc {
|
||||
pid: 999999,
|
||||
start: "x".into(),
|
||||
exes: vec![],
|
||||
},
|
||||
via: "cli".into(),
|
||||
..Default::default()
|
||||
};
|
||||
std::fs::write(&lf.path, serde_json::to_vec(&o).unwrap()).unwrap();
|
||||
alive.store(true, Ordering::SeqCst);
|
||||
assert!(lf.reap_claim().is_none(), "verrou vivant écarté");
|
||||
assert_eq!(read_owner(&lf.path).unwrap(), o);
|
||||
// Prises et rendus répétés pendant que le ménage tourne : jamais écarté.
|
||||
let stop = Arc::new(AtomicBool::new(false));
|
||||
let lf = Arc::new(LockFile::with(
|
||||
tmp.path(),
|
||||
Box::new(|p: &Proc| p.pid == i64::from(std::process::id())),
|
||||
me(),
|
||||
));
|
||||
std::fs::remove_file(&lf.path).unwrap();
|
||||
let (s2, lf2) = (stop.clone(), lf.clone());
|
||||
let reaper = std::thread::spawn(move || {
|
||||
let mut n = 0;
|
||||
while !s2.load(Ordering::SeqCst) {
|
||||
if lf2.reap_claim().is_some() {
|
||||
n += 1;
|
||||
}
|
||||
}
|
||||
n
|
||||
});
|
||||
for i in 0..60 {
|
||||
let l = lf
|
||||
.acquire("apply", true, || async { false })
|
||||
.await
|
||||
.unwrap_or_else(|e| panic!("tour {i} : {e}"));
|
||||
l.set_phase("application");
|
||||
l.release();
|
||||
}
|
||||
stop.store(true, Ordering::SeqCst);
|
||||
assert_eq!(
|
||||
reaper.join().unwrap(),
|
||||
0,
|
||||
"verrou vivant de ce processus écarté"
|
||||
);
|
||||
}
|
||||
|
||||
/// `TestTuneSameExeDeleted`.
|
||||
#[test]
|
||||
fn binaire_remplace() {
|
||||
assert!(same_exe(
|
||||
"/usr/local/bin/loki",
|
||||
"/usr/local/bin/loki (deleted)"
|
||||
));
|
||||
assert!(!same_exe("/usr/local/bin/loki", "/usr/bin/llama-server"));
|
||||
assert!(!same_exe("", ""));
|
||||
}
|
||||
|
||||
/// L'identité d'un processus comme le Go l'écrit : instant de démarrage
|
||||
/// préfixé de l'identifiant du démarrage, binaire sans « (deleted) ».
|
||||
#[cfg(target_os = "linux")]
|
||||
#[test]
|
||||
fn identite_d_un_processus() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path();
|
||||
let p = root.join("proc/77");
|
||||
std::fs::create_dir_all(&p).unwrap();
|
||||
std::fs::create_dir_all(root.join("proc/sys/kernel/random")).unwrap();
|
||||
let stat = |state: &str| {
|
||||
format!("77 (llama (x) server) {state} 1 77 77 0 -1 0 0 0 0 0 0 0 0 0 20 0 1 0 4242 0 0")
|
||||
};
|
||||
std::fs::write(p.join("stat"), stat("S")).unwrap();
|
||||
std::os::unix::fs::symlink("/opt/llama-server (deleted)", p.join("exe")).unwrap();
|
||||
assert_eq!(
|
||||
proc_identity_in(root, 77),
|
||||
Some(("4242".into(), "/opt/llama-server".into()))
|
||||
);
|
||||
std::fs::write(root.join("proc/sys/kernel/random/boot_id"), "b0c1\n").unwrap();
|
||||
assert_eq!(proc_identity_in(root, 77).unwrap().0, "b0c1/4242");
|
||||
std::fs::write(p.join("stat"), stat("Z")).unwrap();
|
||||
assert_eq!(proc_identity_in(root, 77), None, "zombie");
|
||||
assert_eq!(proc_identity_in(root, 0), None);
|
||||
assert_eq!(proc_identity_in(root, 78), None);
|
||||
// Ce processus : vivant, reconnu, et pas un autre au même PID.
|
||||
let me = self_proc();
|
||||
assert!(proc_alive(&me));
|
||||
assert!(!proc_alive(&Proc {
|
||||
start: format!("{}x", me.start),
|
||||
..me.clone()
|
||||
}));
|
||||
assert!(!proc_alive(&Proc {
|
||||
exes: vec!["/usr/bin/autre".into()],
|
||||
..me.clone()
|
||||
}));
|
||||
assert!(proc_alive(&Proc {
|
||||
exes: vec![],
|
||||
start: String::new(),
|
||||
..me
|
||||
}));
|
||||
}
|
||||
|
||||
/// `TestTuneReapOrphan` (la partie processus) : le groupe de l'essai est
|
||||
/// arrêté, SIGTERM d'abord.
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn arret_du_groupe() {
|
||||
use std::os::unix::process::CommandExt;
|
||||
let mut child = std::process::Command::new("sleep")
|
||||
.arg("60")
|
||||
.process_group(0)
|
||||
.spawn()
|
||||
.unwrap();
|
||||
let pid = i64::from(child.id());
|
||||
assert!(pid_alive(pid));
|
||||
let t0 = std::time::Instant::now();
|
||||
let reaped = Arc::new(AtomicBool::new(false));
|
||||
let r = reaped.clone();
|
||||
let waiter = std::thread::spawn(move || {
|
||||
let _ = child.wait();
|
||||
r.store(true, Ordering::SeqCst);
|
||||
});
|
||||
kill_tree(pid, &|| reaped.load(Ordering::SeqCst));
|
||||
waiter.join().unwrap();
|
||||
assert!(t0.elapsed() < Duration::from_secs(5), "SIGTERM ignoré ?");
|
||||
assert!(!pid_alive(pid));
|
||||
}
|
||||
Reference in new issue
Block a user