diff --git a/docs/25-tasas-del-kernel.md b/docs/25-tasas-del-kernel.md index 3da379f1..380596d5 100644 --- a/docs/25-tasas-del-kernel.md +++ b/docs/25-tasas-del-kernel.md @@ -873,7 +873,24 @@ Su precio, concreto: ## 8. Propuestas — lado tawasuyu -Detalle en el handoff espejo. En una línea cada una: +> **✅ LAS SEIS CERRADAS (2026-09-02/03).** Detalle y evidencia en el handoff espejo del otro +> repo (`tawasuyu/HANDOFF-TASAS-KERNEL-DESDE-HAMMER.md` §3). Lo que hay que saber desde acá: +> +> - **W4 dejó tres pendientes y se cerraron como W4.bis (2026-09-03).** El grande no era lo que +> este documento suponía: en arje el límite **no se descartaba, no se pedía**. El camino +> `plain` —el de casi todas las Cards, con los namespaces en `false`— no creaba cgroup, y +> `apply_rlimits_to_cgroup` la llamaba sólo shuma, así que arje nunca escribió `memory.max` +> ni `pids.max`. Ahora hay una sola puerta (`arje-incarnate::cgroup::preparar`) y el hijo del +> camino `plain` entra al cgroup entre `fork` y `execve`. +> - **`docs/state/kernel-contract.toml` quedó al día el 2026-09-03**: alta de +> `proceso-por-descriptor` (pidfd, que se usa desde W1 y no estaba declarada), `consumer` por +> nombre de función sin número de línea, y los `silent` de los cinco cgroups reescritos — +> describían un bug ya arreglado, que es la peor forma que tiene un guardián de mentir. +> - **Lo que sigue sin evidencia**: el OOM-kill del kernel sobre un Ente de arje. Sólo se puede +> ver en una máquina con delegación de cgroups; el clon de desarrollo corre con uid 1001 sobre +> la jerarquía de root. + +En una línea cada una, como se propusieron: - **W1 · `pidfd` para liveness y señales** en `sandokan-local` (hoy `access("/proc/")` y `waitpid(WNOHANG)` en sondeo). Se gana correctitud (carrera de PID) antes que velocidad. diff --git a/docs/state/kernel-contract.toml b/docs/state/kernel-contract.toml index bdfe6f12..bbb25d24 100644 --- a/docs/state/kernel-contract.toml +++ b/docs/state/kernel-contract.toml @@ -17,6 +17,12 @@ # # REGLA DE ALTA. Una capacidad entra acá sólo si hay un consumidor con ruta y símbolo. Lo que # «estaría bueno tener» no es un contrato. +# +# REGLA DE ESCRITURA. Los `consumer` van por **nombre de función, sin número de línea**. Los +# números envejecen con cada commit del otro repo y ya lo hicieron: entre W1 y W4 (2026-09-02) +# los cinco `consumer` de cgroups quedaron apuntando a líneas movidas, y —peor— sus `silent` +# describían un bug ya arreglado («sólo emite un warn!», «silencio total»), que es la forma de +# mentir más cara que tiene un guardián: decir que hay un punto ciego donde ya no lo hay. version = 1 reviewed_against = "7.1.2" @@ -31,13 +37,17 @@ title = "Tope de memoria por unidad" symbols = ["MEMCG"] interface = ["memory.max", "memory.high", "memory.current"] consumer = """ -arje-incarnate::cgroup::{apply_rlimits_to_cgroup:78, set_memory_max_at:204, set_memory_high_at:210} \ -← sandokan-core::Engine::{set_memory_max,set_memory_high} ← sandokan-monitor-llimphi (govierna memory.high)""" +arje-incarnate::cgroup::{preparar, apply_rlimits_to_cgroup, set_memory_max_at, set_memory_high_at} \ +← sandokan-core::Engine::{set_memory_max,set_memory_high} ← sandokan-monitor-llimphi (govierna \ +memory.high) · sandokan-local::medir_entidad lee `memory.current`""" silent = """ -Sin MEMCG los ficheros NO EXISTEN. `apply_rlimits_to_cgroup` sólo emite `warn!(memory.max write \ -failed)` y sigue: la Card corre sin tope y el resto del sistema se entera cuando el OOM killer \ -elige a otro. Es el fallo de CLAUDE.md §3 — un ausente falla ruidosamente, un vacío llega hasta \ -el final diciendo que todo fue bien.""" +YA NO ES SILENCIOSO (tawasuyu, W4 el 2026-09-02 y W4.bis el 2026-09-03). Sin MEMCG los ficheros \ +NO EXISTEN, y ahora eso se dice: `apply_rlimits_to_cgroup` devuelve `Limites{aplicados, faltantes}` \ +con el símbolo que falta —y sólo acusa al kernel si pudo leer `/sys/fs/cgroup/cgroup.controllers` \ +y el controller no estaba—; arje lo marca por unidad (`Degradation::CgroupLimitNotApplied`, visible \ +en `arje-ctl status` como `corriendo⚠`) y shuma lo devuelve en las `warnings` de `create`. \ +Lo que este contrato SÍ sigue sin poder desmentir: nadie vio todavía el OOM-kill del kernel sobre \ +un Ente de arje en una máquina con delegación de cgroups.""" notes = """ PAGADO 2026-08-30 (SDD 25 §7-H1): `-e MEMCG` en la fase configure de linux-generic, linux-metal, \ linux-metal-dual y la derivada linux-gioser, en una sola tanda porque re-hashea el kernel \ @@ -49,24 +59,35 @@ id = "peso-cpu" title = "Reparto proporcional de CPU por unidad" symbols = ["CGROUPS", "CGROUP_SCHED", "FAIR_GROUP_SCHED"] interface = ["cpu.weight", "cpu.stat"] -consumer = "arje-incarnate::cgroup::set_cpu_weight_at:170 (y :60, que descarta el error)" -silent = "`let _ = write(cpu.weight)` en :60: el peso no se aplica y no queda ni un warn." +consumer = "arje-incarnate::cgroup::{preparar, ensure_cgroup, set_cpu_weight_at}" +silent = """ +YA NO ES SILENCIOSO (W4, 2026-09-02): el `let _ = write(cpu.weight)` murió. El peso que no se \ +pudo escribir vuelve al caller nombrando `CGROUP_SCHED`, y el reweight en caliente de `pacha` \ +también (W4.bis: `set_cpu_weight_at` pasa por `escribir_control`, que exige nombrar el símbolo).""" [[capability]] id = "tope-procesos" title = "Tope de procesos por unidad" symbols = ["CGROUPS", "CGROUP_PIDS"] interface = ["pids.max", "pids.current"] -consumer = "arje-incarnate::cgroup::apply_rlimits_to_cgroup:85" -silent = "Mismo patrón que memory.max: sólo un `warn!` y la unidad forkea sin límite." +consumer = """ +arje-incarnate::cgroup::{preparar, apply_rlimits_to_cgroup} · sandokan-local::medir_entidad lee \ +`pids.current`""" +silent = """ +Mismo patrón que memory.max, y arreglado con él: el tope que no se pudo escribir vuelve al caller \ +y marca la unidad. ⚠️ Hasta W4.bis (2026-09-03) había algo peor que un `warn!`: en arje el tope \ +NO SE PEDÍA. El camino `plain` —el de casi todas las Cards, con los namespaces en `false`— no \ +creaba cgroup, y `apply_rlimits_to_cgroup` la llamaba sólo shuma.""" [[capability]] id = "peso-io" title = "Reparto de E/S por unidad" symbols = ["BLK_CGROUP", "BLK_CGROUP_IOCOST"] interface = ["io.weight", "io.cost.qos", "io.cost.model"] -consumer = "arje-incarnate::cgroup::set_io_weight_at:217 (y :64, que descarta el error)" -silent = "`let _ = write(io.weight)`: silencio total." +consumer = "arje-incarnate::cgroup::{preparar, ensure_cgroup, set_io_weight_at}" +silent = """ +YA NO ES SILENCIOSO (W4, 2026-09-02): el `let _ = write(io.weight)` murió, igual que el de \ +`cpu.weight`. El peso que no se pudo escribir vuelve al caller nombrando `BLK_CGROUP`.""" notes = """ `io.weight` con sintaxis «default N» lo publica iocost, no BFQ — por eso el contrato pide \ BLK_CGROUP_IOCOST y no IOSCHED_BFQ (que está apagado en las recetas y no hace falta).""" @@ -76,7 +97,7 @@ id = "cpus-por-unidad" title = "Pinneo de CPUs por unidad" symbols = ["CPUSETS"] interface = ["cpuset.cpus", "cpuset.mems"] -consumer = "arje-incarnate::cgroup::set_cpuset_at:247" +consumer = "arje-incarnate::cgroup::set_cpuset_at" [[capability]] id = "jaula-de-rutas" @@ -140,8 +161,31 @@ title = "taskstats + proc connector (el reemplazo de sondear /proc)" symbols = ["TASKSTATS", "TASK_DELAY_ACCT", "CONNECTOR", "PROC_EVENTS"] interface = ["netlink NETLINK_CONNECTOR", "TASKSTATS_CMD_GET"] consumer = """ -sandokan-local — PREVISTO (SDD 25 T3/W2): hoy sondea `/proc/` a 3,4 µs por proceso. \ -Se declara `wants` porque el kernel ya lo trae por defconfig y perderlo sería una regresión silenciosa.""" +sandokan-local — PREVISTO (SDD 25 T3/W2). ⚠️ Ya no es cierto que sandokan sondee `/proc` para \ +medir una unidad: desde W2 (2026-09-02) la telemetría POR UNIDAD sale del cgroup en O(1) \ +(`arje-incarnate::cgroup::medir` → `memory.current`/`pids.current`/`cpu.stat`). Lo que sigue \ +sondeando `/proc` es el monitor de procesos del SISTEMA (`sysmon-core::scan`), que necesita \ +enumerar todo y es otro consumidor. Se declara `wants` porque el kernel ya lo trae por defconfig \ +y perderlo sería una regresión silenciosa.""" + +[[capability]] +id = "proceso-por-descriptor" +title = "pidfd: agarrar un proceso por descriptor y no por su número" +symbols = [] +interface = ["pidfd_open(2)", "pidfd_send_signal(2)"] +consumer = "sandokan-local::pidfd (backend opcional, `disponible()`) ← LocalEngine::{stop, reap}" +silent = """ +NINGUNO, y por eso `symbols` va vacío: pidfd no depende de ningún `CONFIG_*` — es interfaz del \ +core desde Linux 5.3, así que en un kernel de hammer va a estar siempre. Entra al contrato \ +porque el contrato es la lista de lo que se USA, no la de lo que puede faltar; si mañana el \ +mínimo de kernel baja de 5.3, esta línea es la que lo dice. `sandokan kernel` la mide con una \ +syscall de prueba (no hay `.config` que leer en el Ubuntu ajeno) y tiene tercer estado \ +`NoSeSabe`: un «no pude preguntar» informado como «no está» manda a recompilar un kernel sano.""" +notes = """ +ALTA PEDIDA DESDE TAWASUYU (2026-09-03, handoff de tasas del kernel §3, «dos cosas que le tocan \ +a hammer»). Se usa desde W1 (2026-09-02): `pidfd_open` al encarnar —único momento sin carrera, \ +porque un hijo sin cosechar no cede su número—, el descriptor vive en la `Entity`, `stop` señala \ +por ahí y la rama `ECHILD` de `reap` le pregunta al descriptor. `/proc` queda entero detrás (W5).""" # --------------------------------------------------------------------------------------------- # Perfiles: PARA QUÉ es cada kernel