tawasuyu

sergio/takana

> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…

TOML 949Rust 61Shell 46Markdown 361348 Definitions

hammer-upgrade: GC de generaciones huérfanas — 'hammer upgrade prune' (E4 hardening)

Tras un rollback las generaciones más nuevas quedan inalcanzables (no hay redo).
prune(state, keep) borra esas huérfanas (la cadena viva = current+ancestros vía
parent siempre se conserva); --keep N recorta además la profundidad de rollback.
live_chain() expuesto. 2 tests nuevos + paso prune en upgrade-e2e-test.sh (3->1
gens, árbol vivo intacto).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

a22552e28f0c53c3d5f3c240e6b99d9de1171205 · by sergio · (4 months ago) · parent d23946fa3 · browse the tree at this commit

Semantic delta · 7

crates/hammer-cli/src/main.rs

crates/hammer-upgrade/src/lib.rs

4 files · +125 −4

diff --git a/crates/hammer-cli/src/main.rs b/crates/hammer-cli/src/main.rs
index 03f93ddd..c6e31cbf 100644
--- a/crates/hammer-cli/src/main.rs
+++ b/crates/hammer-cli/src/main.rs
@@ -277,6 +277,13 @@ enum UpgradeCmd {
     Rollback,
     /// Muestra la generación viva y el historial.
     Status,
+    /// Borra generaciones que el rollback ya no alcanza (huérfanas tras un rollback). Recupera espacio.
+    Prune {
+        /// Recorta también la cadena viva a sus N generaciones más nuevas (limita la profundidad de
+        /// rollback). Sin flag, conserva toda la cadena y borra sólo las huérfanas.
+        #[arg(long)]
+        keep: Option<usize>,
+    },
 }
 
 #[derive(Subcommand)]
@@ -871,6 +878,11 @@ fn main() -> anyhow::Result<()> {
                         println!("  {mark} {} → {} ({} ficheros)", g.id, g.tree_dir, g.files.len());
                     }
                 }
+                UpgradeCmd::Prune { keep } => {
+                    let r = hammer_upgrade::prune(&state_root, keep)?;
+                    println!("✓ prune: {} borradas, {} conservadas", r.removed.len(), r.kept.len());
+                    for id in &r.removed { println!("  - {id}"); }
+                }
             }
         }
     }
diff --git a/crates/hammer-upgrade/src/lib.rs b/crates/hammer-upgrade/src/lib.rs
index 030096a3..10092880 100644
--- a/crates/hammer-upgrade/src/lib.rs
+++ b/crates/hammer-upgrade/src/lib.rs
@@ -188,6 +188,57 @@ fn next_id(state_root: &Path) -> Result<u64> {
     Ok(list(state_root)?.iter().map(|m| m.id).max().map_or(1, |m| m + 1))
 }
 
+/// La **cadena viva**: la generación actual seguida de sus ancestros vía `parent`, de la más nueva a
+/// la más vieja. Son las únicas generaciones que el `rollback` puede necesitar (no hay "redo": tras un
+/// rollback las generaciones más nuevas quedan **huérfanas**). Vacía si no hay `current`.
+pub fn live_chain(state_root: &Path) -> Result<Vec<u64>> {
+    let mut chain = Vec::new();
+    let mut cur = current(state_root)?;
+    let mut guard = 0usize; // anti-ciclo defensivo (un manifiesto manipulado no debe colgar el proceso)
+    while let Some(id) = cur {
+        if chain.contains(&id) || guard > 100_000 {
+            break;
+        }
+        chain.push(id);
+        guard += 1;
+        cur = get(state_root, id)?.and_then(|m| m.parent);
+    }
+    Ok(chain)
+}
+
+/// Reporte de un [`prune`].
+#[derive(Debug, Clone, Default)]
+pub struct PruneReport {
+    pub removed: Vec<u64>,
+    pub kept: Vec<u64>,
+}
+
+/// Recupera espacio borrando generaciones (manifiesto + `backup/`) que el rollback ya no puede
+/// alcanzar. **Siempre** se conservan las de la [cadena viva](live_chain); el resto (huérfanas tras un
+/// rollback) se borran. Si `keep` es `Some(n)`, además se **recorta la cadena** a sus `n` generaciones
+/// más nuevas — esto limita la profundidad de rollback disponible (como `delete-generations` en NixOS),
+/// así que `None` (conservar toda la cadena) es lo seguro por defecto. Idempotente.
+pub fn prune(state_root: &Path, keep: Option<usize>) -> Result<PruneReport> {
+    let chain = live_chain(state_root)?;
+    let keep_set: std::collections::BTreeSet<u64> = match keep {
+        Some(n) => chain.iter().take(n).copied().collect(),
+        None => chain.iter().copied().collect(),
+    };
+    let mut report = PruneReport::default();
+    for m in list(state_root)? {
+        if keep_set.contains(&m.id) {
+            report.kept.push(m.id);
+            continue;
+        }
+        let gen_base = generations_dir(state_root).join(m.id.to_string());
+        std::fs::remove_dir_all(&gen_base)?;
+        report.removed.push(m.id);
+    }
+    report.removed.sort_unstable();
+    report.kept.sort_unstable();
+    Ok(report)
+}
+
 // ---------------------------------------------------------------------------------------------------
 // Enumeración de árboles.
 // ---------------------------------------------------------------------------------------------------
@@ -715,4 +766,50 @@ mod tests {
         assert_eq!(evs.len(), 2);
         assert!(evs.iter().all(|e| matches!(&e.by.source, Source::HammerHydrate { artifact } if artifact == &dir)));
     }
+
+    #[test]
+    fn prune_removes_orphans_but_keeps_live_chain() {
+        let tmp = tempfile::tempdir().unwrap();
+        let store = tmp.path().join("store");
+        let root = tmp.path().join("root");
+        let state = tmp.path().join("state");
+        let v1 = seal_tree(&store, &h64("a1"), "v1", |w| write(&w.join("f"), b"1"));
+        let v2 = seal_tree(&store, &h64("a2"), "v2", |w| write(&w.join("f"), b"2"));
+        let v3 = seal_tree(&store, &h64("a3"), "v3", |w| write(&w.join("f"), b"3"));
+
+        apply(&store, &v1, None, &root, &state, None).unwrap(); // gen 1
+        apply(&store, &v2, None, &root, &state, None).unwrap(); // gen 2 (parent 1)
+        apply(&store, &v3, None, &root, &state, None).unwrap(); // gen 3 (parent 2)
+        // rollback dos veces ⇒ current=1; las gens 2 y 3 quedan HUÉRFANAS (rollback no las alcanza).
+        rollback(&root, &state, None).unwrap();
+        rollback(&root, &state, None).unwrap();
+        assert_eq!(current(&state).unwrap(), Some(1));
+        assert_eq!(live_chain(&state).unwrap(), vec![1]);
+
+        let r = prune(&state, None).unwrap();
+        assert_eq!(r.removed, vec![2, 3], "huérfanas borradas");
+        assert_eq!(r.kept, vec![1], "cadena viva conservada");
+        assert_eq!(list(&state).unwrap().iter().map(|m| m.id).collect::<Vec<_>>(), vec![1]);
+        // idempotente.
+        assert!(prune(&state, None).unwrap().removed.is_empty());
+    }
+
+    #[test]
+    fn prune_with_keep_trims_chain_depth() {
+        let tmp = tempfile::tempdir().unwrap();
+        let store = tmp.path().join("store");
+        let root = tmp.path().join("root");
+        let state = tmp.path().join("state");
+        for (hx, nm, by) in [("b1", "v1", b"1"), ("b2", "v2", b"2"), ("b3", "v3", b"3")] {
+            let d = seal_tree(&store, &h64(hx), nm, |w| write(&w.join("f"), by));
+            apply(&store, &d, None, &root, &state, None).unwrap();
+        }
+        // current=3, cadena=[3,2,1]. keep=Some(2) ⇒ conserva 3 y 2, borra 1.
+        assert_eq!(live_chain(&state).unwrap(), vec![3, 2, 1]);
+        let r = prune(&state, Some(2)).unwrap();
+        assert_eq!(r.kept, vec![2, 3]);
+        assert_eq!(r.removed, vec![1]);
+        // gen 1 borrada del disco; 2 y 3 siguen.
+        assert_eq!(list(&state).unwrap().iter().map(|m| m.id).collect::<Vec<_>>(), vec![2, 3]);
+    }
 }
diff --git a/docs/13-release-engineering.md b/docs/13-release-engineering.md
index c2252e92..d2c2aae8 100644
--- a/docs/13-release-engineering.md
+++ b/docs/13-release-engineering.md
@@ -79,8 +79,11 @@ kernel en `/boot`, los módulos GRUB en `/boot/grub/i386-pc` y los pasos `grub-m
   `HammerHydrate{artifact=tree_dir}` (replay-able). Verificación opcional de integridad: si el llamador
   anuncia el `of_tree` esperado (de un índice de **mirror E3** o release firmada), el apply lo exige antes
   de tocar el root. Maneja ficheros regulares + symlinks; valida E2E en host con `scripts/upgrade-e2e-test.sh`
-  (apply v1→v2→rollback→rollback contra el binario real). **Endurecimiento pendiente:** journal de
-  *intención* + replay para sobrevivir un kernel-panic a media escritura; GC de generaciones viejas
-  (prune); cableado del init de boot para que el rollback sea seleccionable al arranque.
+  (apply v1→v2→rollback→rollback contra el binario real). **GC de generaciones ✅** (`hammer upgrade
+  prune [--keep N]` / `hammer_upgrade::prune`): borra las generaciones que el rollback ya no alcanza
+  (huérfanas tras un rollback — las de la [cadena viva](#) siempre se conservan); `--keep N` además
+  recorta la cadena a sus N más nuevas (limita la profundidad de rollback, como `delete-generations` en
+  NixOS). **Endurecimiento pendiente:** journal de *intención* + replay para sobrevivir un kernel-panic a
+  media escritura; cableado del init de boot para que el rollback sea seleccionable al arranque.
 - **E5 — ISO/medio de arranque:** medio live para correr el instalador (xorriso/grub-mkrescue;
   herramental a construir).
diff --git a/scripts/upgrade-e2e-test.sh b/scripts/upgrade-e2e-test.sh
index 7512b94a..f556520e 100755
--- a/scripts/upgrade-e2e-test.sh
+++ b/scripts/upgrade-e2e-test.sh
@@ -99,5 +99,14 @@ LINES="$(wc -l < "$JOURNAL/mutations.jsonl")"
 [ "$LINES" -gt 0 ] || { echo "FALLO: diario vacío" >&2; exit 1; }
 echo "  ok: $LINES eventos en el diario"
 
+echo "==> prune: tras los dos rollback completos las gens 1 y 2 quedaron huérfanas (current=3)"
+GENS_ANTES="$(ls "$STATE/generations" | wc -l)"
+up prune
+GENS_DESPUES="$(ls "$STATE/generations" | wc -l)"
+[ "$GENS_ANTES" -gt "$GENS_DESPUES" ] || { echo "FALLO: prune no borró huérfanas ($GENS_ANTES→$GENS_DESPUES)" >&2; exit 1; }
+echo "  ok: prune $GENS_ANTES→$GENS_DESPUES generación(es)"
+# el árbol vivo (gen 3 = v1) sigue intacto tras el prune.
+assert "$ROOT/usr/bin/ls" "LS-v1" "post-prune"
+
 echo
-echo "✓✓ E4 upgrade E2E VERDE: apply/rollback atómicos + restauración exacta + idempotencia + diario"
+echo "✓✓ E4 upgrade E2E VERDE: apply/rollback atómicos + restauración exacta + idempotencia + diario + prune"