Copias originales: cancelación y checkpoints durables¶
20 de octubre de 2026. Estado vigente, 23:26:57 CEST: PR87 instalada en VM208 y verificada independientemente después de un reset natural del padre, sobre el backend PR86. La instalación fue a las 23:23:47 CEST; compilación nativa correcta. El primer postcheck quedó UNKNOWN y se conserva. La instalación inicial de cancelación fue a las 02:09:03 CEST. No se ejecutó ninguna copia ni se modificaron el estado de originales o la tarea RunWisp durante estas instalaciones. Las secciones de preparación conservan la evidencia de la fase previa de fuente; el despliegue y su rollback se registran al final.
Problema acreditado en código¶
copy_with_heartbeat creaba el proceso en una sesión nueva antes de entrar en
el bloque protegido e instalar manejadores. Una señal en esa ventana podía
terminar el productor sin pasar por la limpieza de su grupo local. Además,
SIGHUP no participaba en el mecanismo de cancelación. Esto es un defecto de
ciclo de vida observado en la fuente; no se atribuye a él ningún incidente real.
La revisión independiente detectó una segunda ventana: al instalar SIG_IGN señal por señal, otra señal todavía no ignorada podía interrumpir la entrada a la limpieza. La restauración de los manejadores también necesitaba completarse antes de entregar señales pendientes.
Cambio acotado¶
- Se valida el estado de grupo y se exige el hilo principal antes de crear hijos.
- TERM, INT y HUP permanecen bloqueadas mientras se instalan los manejadores y se crea y registra el proceso. El hijo restaura la máscara anterior antes de exec; rsync y SSH no heredan el bloqueo temporal del productor.
- Las señales pendientes se entregan cuando el productor ya conoce su proceso. La limpieza actúa exclusivamente sobre la sesión local creada por esa llamada.
- Tanto la transición conjunta a SIG_IGN como la restauración conjunta de los manejadores quedan protegidas por la misma máscara. Las señales repetidas no interrumpen TERM/KILL/reap.
- El reap posterior a SIGKILL tiene un timeout de cinco segundos; antes esperaba indefinidamente. Si falla, la excepción original se conserva y el fallo de limpieza queda encadenado como causa.
No se cambian la selección de muestras, los cuatro grupos, los argumentos de rsync, la publicación del dump, el ledger ni la programación RunWisp.
Verificación local¶
python3 scripts/original-backups/test_backup.py: 36 tests, OK; uno omitido.
La omisión existente exige rsync 3.2 o posterior para su fixture de argumentos;
la versión local de macOS no satisface ese requisito. El coordinador repitió la
misma suite con el mismo resultado.
La revisión independiente de otro agente concluyó PASS sobre los hashes candidatos: repitió los 36 tests del backend (una omisión) y los 36 del publisher, y también obtuvo el mismo resultado del backend con Python 3.12. La comprobación de gramática Python 3.10 pasó; no equivale a ejecutar las pruebas en ese runtime.
Las siete pruebas añadidas cubren:
- TERM, INT y HUP reales durante la ventana de creación/registro, con un árbol sintético de procesos; el grupo termina y el hijo directo queda recogido.
- La máscara observada por el hijo tras exec coincide con la máscara original.
- Un fallo de creación restaura máscara y los tres manejadores.
- INT y HUP repetidas durante wait no interrumpen la limpieza de un árbol real.
- Un reap que no puede confirmarse vence su plazo y conserva el error original.
- HUP inyectada tras instalar el primer SIG_IGN no evita limpiar el grupo.
- INT inyectada tras restaurar el primer manejador sólo se entrega cuando los tres manejadores anteriores ya están restaurados.
Todos los archivos y procesos de estas pruebas son fixtures locales sintéticos; no son archivos personales ni procesos del homelab.
Identidad y recuperación de fuente¶
| Archivo | SHA256 candidato | SHA256 anterior |
|---|---|---|
backup_originals.py |
<REDACTED> |
<REDACTED> |
test_backup.py |
<REDACTED> |
<REDACTED> |
La reversión de la fase de fuente consiste en revertir únicamente el diff de esos dos archivos, verificando antes sus hashes y conservando los cambios ajenos. No hay rollback runtime ejecutado; el respaldo del despliegue posterior se describe al final.
Límites pendientes¶
La suite se ejecutó en los Python locales descritos; el despliegue posterior acreditó compilación, no ejecución de la suite, en Python 3.10.12 de VM208. El diseño usa las APIs POSIX de señales y proceso existentes en ese entorno; no es portable a Windows ni permite invocación desde un hilo secundario.
SIGKILL, una caída del host y procesos en espera ininterrumpible no pueden resolverse mediante manejadores Python. Si el reap vence el plazo, se informa error y no se acredita limpieza completa. Matar el grupo local tampoco prueba la terminación instantánea de un receptor remoto; la exclusión por generación del receptor conserva su responsabilidad independiente.
Despliegue autorizado y confirmado por el coordinador¶
A las 02:09:03 CEST del 20 de octubre de 2026, el coordinador confirmó la
instalación del backend SHA256
<REDACTED>.
La validación nativa con Python 3.10.12 compiló correctamente el código;
no se ejecutó la suite de pruebas ni un job de copias en ese runtime.
Antes de escribir se exigieron el baseline 9d27f682…, UID/GID 0/0, modo 0755,
archivo regular con un único enlace y el mismo inode en la comprobación previa
al reemplazo. Se tomó el flock exclusivo existente y se comprobó que no había
una copia activa. La instalación usó un temporal, rename y fsync.
Se conservó el rollback privado en
VM208:/root/rollbacks/originals-cancel-20261020T000903Z: directorio 0700 y
original, preflight y receipt 0600, con fsync. La recuperación debe revalidar el
hash candidato y restaurar únicamente ese original, preservando owner y modo,
bajo las mismas guardas de exclusión. El rollback no se ejecutó.
El estado de originales mantuvo SHA 613875… y la configuración RunWisp SHA
63af… intactos. El helper de publicación no cambió. No hubo ejecución de job,
reinicio de servicio ni cambio de programación. Esta fase fue realizada y
acreditada por el coordinador; el agente de fuente sólo actualizó este informe.
Guardado durable integrado en fuente, 20 de octubre¶
La PR87 se fusiona a las
20:49:39 CEST en <REDACTED>.
CI39477902679
correcta en Linux Python 3.10/3.12 y promtool; revisión independiente correcta.
Integración local: 78 pruebas de backend, una omisión conocida de GNU rsync,
más 42 regresiones de métricas/capacidad/configuración correctas. MkDocs estricto
sin avisos. No se instala este backend ni se ejecuta una copia como prueba.
La auditoría offline del backend 8581a6dc… encontró que save() usaba
write/chmod/replace sin fsync. Un reemplazo visible no acreditaba persistencia
ante un reset. No se ha probado corrupción real ni pérdida de checkpoints.
El writer de historia legacy ya tenía sus propios fsync; éstos no cubrían
last.json, sus heartbeats, los manifiestos ordinarios o last-invocation.json.
El candidato parte de b27e196 y conserva save(path, data), la serialización
JSON, copias, calendario y heartbeat de diez segundos. Escribe un temporal
exclusivo 0600 en el mismo directorio, hace flush/fsync del archivo, reemplaza
el destino y sincroniza el directorio. Sus nombres quedan anclados al FD del
padre existente; los ancestros se consideran confiables. No acredita la
persistencia de ancestros recién creados ni protege contra escritores externos
que ignoren el lock.
CheckpointError, compatible con OSError, sólo revela fase y estado del
reemplazo: false antes de intentarlo, true si retornó, unknown si se interrumpió
su llamada antes de confirmar el resultado. Un fallo anterior conserva el
checkpoint previo y limpia sólo su temporal propio; una colisión o sustitución
ajena conserva el artefacto ajeno. Un fallo después de replace no restaura el
archivo previo ni reintenta guardar automáticamente. La nueva entrada visible
puede tener durabilidad incierta.
KeyboardInterrupt, SystemExit y BackupInterrupted conservan su objeto/tipo y
marcan el origen del checkpoint para impedir otro guardado automático. El
cleanup no oculta una cancelación con un error ordinario. Si una operación
externa falla y también falla el checkpoint de error, se conserva la excepción
primaria y se emite un aviso genérico sin valores privados; los controles de
cancelación secundarios no se capturan como errores ordinarios.
La regresión de orden falla contra la fuente anterior: sólo observa replace.
Con el candidato, 78 pruebas por intérprete, una omitida por falta de GNU
rsync, pasan en Python3.12 y3.14; diecinueve casos nuevos cubren fsync/replace,
escritura parcial, commit incierto, colisiones, limpieza, error secundario y
SIGTERM real durante un heartbeat con limpieza del grupo de copia sintético.
La gramática Python3.10 pasa; no se ha ejecutado aquí la suite con ese intérprete.
Evidencia privada originals-checkpoint-review-20261020-qthpck3d/, con logs
before/python312/python314 y recibos de sus hashes.
A las 20:49 CEST este cambio quedaba sólo en fuente, integrado y revisado, pendiente de instalación acotada bajo las guardas y el hold existentes. No se ejecutaron backups, publicaciones, colectores, NAS, jobs o pruebas de pérdida de energía. El coste de fsync por heartbeat no estaba medido en esa fase; no se modifica su intervalo ni se presenta como solución a los resets. Sustituir el mismo ledger con heartbeats sin fsync tampoco conservaría por sí solo el checkpoint durable anterior.
Admisión mínima y despliegue de PR87, 23:21–23:23 CEST¶
El coordinador ejecuta a las 23:21:23 CEST tres checkpoints sintéticos de
64 KiB en un directorio privado nuevo, hermano del estado y en el mismo
dispositivo local que éste y el backend. No se leen contenidos del estado ni
se importa o ejecuta la aplicación; no hay NAS ni jobs. Cada muestra contiene
un fsync del archivo y otro del directorio. Totales: 6,226 / 9,883 / 6,810 ms;
los artefactos propios se eliminan y los guards de propietario/boot/backend
permanecen válidos. Recibo privado:
~/.local/state/originals-fsync-admission-2m_zkvjw/bench-result.json, SHA256
<REDACTED>.
Tres muestras admiten esta intervención acotada; no acreditan picos, rendimiento
bajo carga, persistencia física ante reset ni reparación de su causa.
A las 23:23:47 CEST se realiza una única instalación de
/home/monxas/scripts/backup_originals.py, de PR86 SHA256
<REDACTED>
a PR87 SHA256
<REDACTED>.
Archivo regular, enlace único, root:root, modo0755, 41366 bytes; compilación
nativa correcta sin importar/ejecutar el backend. La comparación AST previa
acredita cambios sólo en save/run_backup y dos definiciones auxiliares;
imports, copia, métricas y programación permanecen iguales.
Se usan preflight fresco, flock exclusivo del lock existente, dos comprobaciones
de cero escritores, CAS de identidad completa/hash, reemplazo y fsync. Ledger,
manifests y los demás controles seleccionados coinciden antes/después. Durante esa instalación PVE208
sigue qemu/running en proxmox; propietario antes/después y boot padre
<REDACTED_UUID> coinciden. Boot VM208:
<REDACTED_UUID>. No se ejecutan copias, publicación,
colector, reinicios ni cambios de RunWisp/configuración.
Rollback privado durable:
/root/rollback-originals-checkpoint-20261020T212347Z-lq5gdmmk/backend.before.
Conserva el backend PR86 previo, en directorio0700 y archivo0600. No se ejecuta
rollback automático: un fallo incierto exige lectura independiente y revisión,
sin repetir la instalación. Recibos locales privados en
~/.local/state/originals-durable-checkpoint-install-ofk29bx3/:
install-result.json, SHA256
<REDACTED>,
y owner-before.json/owner-after.json, ambos SHA256
<REDACTED>.
Reset posterior y comprobación independiente, 23:24–23:26 CEST¶
El primer postcheck no llegó a leer el huésped: el guard del padre rechazó el
boot nuevo después de otro reset a las 23:24:41 CEST. Se conserva
postverify-aborted.json como UNKNOWN, SHA256
<REDACTED>.
No se convierte ese intento en PASS ni se repite la instalación. El mecanismo
watchdog se acreditó por separado; la causa de la pérdida de keepalive sigue
sin determinar. No se atribuye el reset a fsync ni se declara esa causa reparada.
El arranque automático de VM208 completó a las 23:25:20 CEST. Los guards nuevos
acreditan padre <REDACTED_UUID> y huésped
<REDACTED_UUID>. Las identidades de boot82d/79b3 de la
instalación anterior quedan como evidencia histórica, no como boots actuales.
Una única lectura independiente a las 23:26:57 CEST confirma el backend
PR87 exacto 0fca4c…7488, con identidad completa instalada e inode 829863;
el rollback conserva PR86 f33f1b…6651, con metadata verificada. El recibo
privado del huésped conserva fase installed, SHA256
<REDACTED>.
Lock y cuatro controles coinciden en bytes e identidad completa; diferencias
vacías. Propietario y boots nuevos se comprueban antes/después. Sin importar
la aplicación ni invocar métricas, jobs, NAS o escrituras.
Recibo independiente postreset-independent.json en el mismo stage privado,
SHA256 <REDACTED>,
estado independently_verified. Acredita persistencia observada del archivo
instalado y sus controles tras este reset natural. No es una prueba extensiva
de pérdida de energía/picos, de un checkpoint escrito por una copia real ni de
recuperación NAS; tampoco establece causalidad entre la instalación y el reset.