No hay nada que diga “disfruta tu viernes” como una canalización de despliegue que funcionó durante meses y de repente tropieza con: Text file busy. Mismo código, mismos hosts, mismo trabajo de CI. Ahora la release falla a mitad de copiar un archivo y deja tu servicio en ese estado especial donde está “arriba” y “no exactamente la versión que querías”.
Este es uno de esos errores de Linux que parecen mezquinos hasta que entiendes qué está protegiendo el kernel. Una vez que lo entiendes, dejarás de intentar “forzar sobrescribir el binario” (por favor, no lo hagas) y cambiarás a patrones de despliegue que no peleen con el SO.
Qué significa realmente “Text file busy” (ETXTBSY)
En Linux, Text file busy suele ser la manifestación en espacio de usuario de ETXTBSY. El kernel lo devuelve cuando un proceso intenta modificar un archivo ejecutable que está siendo ejecutado (o, en algunos casos, abierto de una forma que indica que se está usando como texto de programa).
“Text” es vocabulario histórico de Unix para el segmento de código ejecutable. “Busy” significa “alguien lo está ejecutando, no lo rayes”.
Hay una matización importante: Linux normalmente te permite unlinkear (eliminar) o renombrar ejecutables mientras se están ejecutando. El proceso en ejecución mantiene una referencia abierta al archivo; la entrada de directorio puede desaparecer. Ese es el truco clásico de Unix. Pero Linux es mucho menos entusiasta respecto a que escribas nuevos bytes en el mismo inode de un ejecutable en ejecución. Ahí es donde entra ETXTBSY. Es el SO diciendo: “puedes reemplazar; no puedes mutar en el lugar”.
Si tu estrategia de despliegue es “copiar el nuevo binario sobre la ruta antigua”, estás haciendo una mutación en el lugar. A veces funciona; a veces no; a veces solo falla cuando menos lo deseas. Y Debian 13, con kernels modernos y sistemas de archivos comunes, hará cumplir ese límite sin pestañear.
Una cita que vale la pena pegar en una nota junto a tus scripts de despliegue: parafraseando: «la esperanza no es una estrategia»
— atribuida en círculos de ops a Gene Kranz, generalmente como advertencia contra la ingeniería basada en deseos. Si tu release depende de “ojalá el archivo no esté siendo ejecutado cuando lo sobrescriba”, estás confiando en la esperanza.
Cómo se presenta en la vida real
Lo verás en algunas formas comunes:
cp: cannot create regular file '...': Text file busymv: cannot move 'new' to 'old': Text file busy(menos común, pero aparece con ciertas semánticas de FS)bash: ./mybin: Text file busy(cuando un script o automatización intenta ejecutar algo que se está reemplazando)- Actualizaciones de paquetes que fallan cuando un script del mantenedor intenta reemplazar un binario en uso de forma no atómica
Broma #1: ETXTBSY es Linux diciéndote educadamente “veo lo que intentas hacer, y elijo violencia contra tu script de despliegue”.
Por qué fallan los despliegues en Debian 13: los mecanismos reales
Este error no es “algo de Debian”. Es una cosa de Unix y Linux que se vuelve visible cuando tu método de despliegue es incompatible con cómo se comportan el kernel, el sistema de archivos y el cargador de runtime.
Mecanismo 1: sobrescritura en el lugar de un ejecutable en ejecución
El modo de fallo canónico es dolorosamente simple:
- El servicio está ejecutando
/opt/myapp/myapp. - El despliegue hace
cp myapp /opt/myapp/myappo descarga en la misma ruta de archivo. - El kernel rechaza la escritura/reemplazo porque el archivo está “ocupado” como texto ejecutable.
Algunas herramientas son más engañosas de lo que parecen. Una utilidad de copia “segura” podría abrir el destino para escribir y truncarlo antes de copiar. Ese es exactamente el tipo de mutación en el lugar que ETXTBSY está diseñado para detener.
Mecanismo 2: bind mounts y semánticas de volúmenes en contenedores
Si haces un bind-mount de un directorio del host dentro de un contenedor, a menudo despliegas escribiendo en ese directorio desde el host (o desde otro contenedor). El proceso de la app dentro del contenedor tiene el archivo abierto/ejecutándose, así que el reemplazo desde el host puede chocar con ETXTBSY. El límite del contenedor no cambia las semánticas del kernel; solo cambia quién recibe el reclamo.
Mecanismo 3: bibliotecas compartidas, loaders y “no es solo el binario”
A veces lo que estás sobrescribiendo no es el ejecutable principal. Es un plugin, una biblioteca compartida o una herramienta auxiliar invocada durante el despliegue.
Linux suele aceptar actualizar librerías compartidas mediante reemplazo atómico (archivo nuevo, renombrar en su lugar). Pero si tu herramienta de despliegue escribe directamente en archivos .so en el lugar, puedes desencadenar el mismo comportamiento de ocupado. Además, un error común es reemplazar /usr/bin/python (o un runtime) mientras un script de mantenimiento todavía lo está usando. No es teórico; así es como obtienes scripts de actualización explotando a mitad de ejecución.
Mecanismo 4: casos límite de sistemas de archivos (NFS, overlayfs, almacenamiento en red “útil”)
Los sistemas de archivos locales (ext4, xfs) se comportan de forma predecible: rename es atómico; las semánticas de unlink son estables; la aplicación de ETXTBSY coincide con las expectativas de Linux. Los sistemas de archivos en red y las capas overlay pueden introducir comprobaciones extra o semánticas ligeramente diferentes. NFS en particular tiene historia de comportamiento de “silly rename” y complejidad de coherencia de caché; puede convertir un reemplazo limpio en una condición de ocupado extraña o visibilidad retardada.
Esto importa porque muchos “despliegues Debian 13” son en realidad “Debian 13 sobre una nueva capa de almacenamiento que introdujimos discretamente”. Cuando ves ETXTBSY, pregúntate siempre: ¿cambió el almacenamiento o el runtime?
Mecanismo 5: tu pipeline de despliegue ejecuta accidentalmente el archivo mientras lo reemplaza
Aquí hay uno más embarazoso: el script de despliegue comprueba la versión ejecutando myapp --version desde la misma ruta que va a sobrescribir. Si el script ejecuta ese binario mientras otro paso empieza a sobrescribirlo, enhorabuena: te has ganado una carrera contigo mismo.
Broma #2: La forma más rápida de reproducir ETXTBSY es programar tu despliegue justo en el momento en que le prometiste a Ventas que sería “un cambio rápido”.
Guía rápida de diagnóstico
Si estás de guardia y la canalización está en rojo, haz esto en orden. El objetivo es identificar qué proceso está manteniendo el ejecutable (o la librería) abierto, y si tu método de despliegue está haciendo escrituras en el lugar.
1) Identifica la ruta exacta que disparó ETXTBSY
- Desde los logs de CI: captura la ruta de archivo en la línea de error.
- Desde logs del sistema: identifica el comando fallido y el objetivo.
2) Encuentra quién está usando ese archivo (proceso + PID)
lsofofusersobre la ruta.- Confirma si es un mapeo ejecutable (
txt) o solo un descriptor abierto.
3) Determina el patrón de despliegue: reemplazo atómico vs sobrescritura en el lugar
- Si ves
cpdirectamente en la ruta final, estás sobrescribiendo en el lugar. - Si ves
rsynca un directorio en vivo, podrías estar reescribiendo en el lugar dependiendo de las flags. - Si ves “escribe temporal y luego renombra” y aún obtienes ETXTBSY, sospecha casos límite de filesystem/contenedor.
4) Decide la solución inmediata menos arriesgada
- Si el servicio puede reiniciarse: para/reinicia el servicio y redeploy usando intercambio atómico.
- Si reiniciar es riesgoso: despliega en un nuevo directorio de release, cambia mediante symlink y luego reinicia de forma controlada (o usa activación por socket / handoff).
- Si es una actualización de paquete: planifica reinicio, o usa el flujo de
needrestart; no fuerces escrituras sobre el archivo.
5) Aplica la corrección permanente
- Cambia el despliegue a directorios de release inmutables + intercambio atómico de puntero.
- O mueve el ejecutable a una ruta versionada y mantén un symlink estable.
- O asegúrate de que el archivo que se reemplaza nunca se modifique en el lugar (descarga a temporal + rename).
Tareas prácticas: comandos, salidas y la siguiente decisión
Estas son tareas probadas en campo. Cada una incluye un comando, salida de ejemplo, qué significa y la decisión que tomas después. Ejecútalas como un usuario con privilegios suficientes (root o mediante sudo) en el host afectado.
Task 1: Confirmar que la syscall fallida es ETXTBSY (traza con strace)
cr0x@server:~$ strace -f -o /tmp/deploy.strace cp -f ./myapp /opt/myapp/myapp
cp: cannot create regular file '/opt/myapp/myapp': Text file busy
cr0x@server:~$ tail -n 5 /tmp/deploy.strace
openat(AT_FDCWD, "/opt/myapp/myapp", O_WRONLY|O_CREAT|O_TRUNC, 0666) = -1 ETXTBSY (Text file busy)
write(2, "cp: cannot create regular file"..., 72) = 72
exit_group(1) = ?
+++ exited with 1 +++
Significado: Tu herramienta de despliegue está intentando abrir el destino con O_TRUNC (sobrescritura en el lugar). El kernel lo bloquea por un mapeo de ejecución.
Decisión: Deja de hacer sobrescrituras en el lugar. Cambia a “escribir temporal y luego renombrar” o a swap de directorio de release.
Task 2: Encontrar qué proceso está ejecutando o mapeando el archivo (lsof)
cr0x@server:~$ sudo lsof /opt/myapp/myapp | head
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
myapp 14231 myapp txt REG 259,2 18239440 1310723 /opt/myapp/myapp
Significado: PID 14231 está ejecutando /opt/myapp/myapp (FD tipo txt).
Decisión: No puedes sobrescribir ese inode. O reinicias/paras ese proceso, o despliegas un nuevo inode y haces swap de punteros.
Task 3: Confirmar todos los procesos que referencian el inode (fuser con verbose)
cr0x@server:~$ sudo fuser -v /opt/myapp/myapp
USER PID ACCESS COMMAND
/opt/myapp/myapp: myapp 14231 ...e. myapp
Significado: ...e. indica acceso de ejecución.
Decisión: Si aparecen múltiples PIDs, tratas con trabajadores múltiples o un supervisor (systemd, gunicorn, etc.). Planifica el reinicio en consecuencia.
Task 4: Comprobar si la ruta del binario es un symlink (y a qué apunta)
cr0x@server:~$ readlink -f /opt/myapp/myapp
/opt/myapp/releases/2025-12-30_121500/myapp
Significado: La ruta estable es un symlink hacia un directorio de release. Esto es buena noticia: puedes intercambiar el symlink de forma atómica.
Decisión: Despliega la nueva release en un directorio nuevo, luego cambia el symlink. No toques el directorio de release existente en el lugar.
Task 5: Inspeccionar la unidad de servicio para ExecStart y WorkingDirectory (systemd)
cr0x@server:~$ systemctl cat myapp.service
# /etc/systemd/system/myapp.service
[Unit]
Description=MyApp API
[Service]
User=myapp
WorkingDirectory=/opt/myapp/current
ExecStart=/opt/myapp/current/myapp --config /etc/myapp/config.yaml
Restart=on-failure
[Install]
WantedBy=multi-user.target
Significado: El servicio se ejecuta desde /opt/myapp/current. Si tu despliegue modifica ese directorio en el lugar, corres el riesgo de ETXTBSY (y de actualizaciones parciales).
Decisión: Haz que /opt/myapp/current sea un symlink a un directorio de release inmutable, y solo cambia el symlink.
Task 6: Determinar si el proceso en ejecución usa un archivo eliminado (clásico “unlink pero sigue en ejecución”)
cr0x@server:~$ sudo ls -l /proc/14231/exe
lrwxrwxrwx 1 root root 0 Dec 30 12:18 /proc/14231/exe -> /opt/myapp/releases/2025-12-30_121500/myapp (deleted)
Significado: Ya reemplazaste/unlinkeaste el archivo, pero el proceso aún ejecuta el inode antiguo (ahora borrado). Esto es comportamiento normal de Unix.
Decisión: Planifica un reinicio si necesitas que la nueva versión esté activa. No intentes “arreglar” sobrescribiendo; no afectará al proceso en memoria.
Task 7: Verificar el número de inode y si los intentos de despliegue reescriben el mismo inode
cr0x@server:~$ stat -c 'inode=%i path=%n' /opt/myapp/myapp
inode=1310723 path=/opt/myapp/myapp
cr0x@server:~$ sudo ls -li /opt/myapp/myapp
1310723 -rwxr-xr-x 1 myapp myapp 18239440 Dec 30 12:15 /opt/myapp/myapp
Significado: Tienes un inode específico. Si tu despliegue hace escrituras en el lugar, apunta a este inode y desencadena ETXTBSY.
Decisión: Asegura que el despliegue cree un inode nuevo (escribe en otro nombre de archivo y luego renombra/swap de symlink).
Task 8: Comprobar cómo se comporta tu herramienta de despliegue (las flags de rsync importan)
cr0x@server:~$ rsync --version | head -n 2
rsync version 3.2.7 protocol version 32
cr0x@server:~$ rsync -av --inplace ./myapp /opt/myapp/myapp
rsync: [receiver] open "/opt/myapp/myapp" failed: Text file busy (26)
rsync error: errors selecting input/output files, dirs (code 3) at main.c(772) [Receiver=3.2.7]
Significado: --inplace es la prueba de fuego. Fuerza la modificación en el lugar.
Decisión: Elimina --inplace. Prefiere --delay-updates y desplegar a una ruta de staging, o usa directorios de release.
Task 9: Determinar si el sistema de archivos es overlayfs / capa de contenedor (mount)
cr0x@server:~$ mount | grep -E '/opt/myapp|overlay'
overlay on / type overlay (rw,relatime,lowerdir=/var/lib/docker/overlay2/l/...,upperdir=/var/lib/docker/overlay2/.../diff,workdir=/var/lib/docker/overlay2/.../work)
tmpfs on /run type tmpfs (rw,nosuid,nodev,size=328284k,mode=755)
Significado: Estás en un entorno overlayfs (común en contenedores). Algunas operaciones se comportan distinto, y “reemplazar en el lugar” sigue siendo mala idea.
Decisión: No mutar ejecutables dentro de un filesystem de contenedor en ejecución. Construye una nueva imagen y redeploya, o usa un patrón de release-dir dentro de un volumen escribible con swap atómico de puntero.
Task 10: Si son paquetes del sistema, ver qué intentó hacer dpkg (logs de dpkg)
cr0x@server:~$ sudo tail -n 8 /var/log/dpkg.log
2025-12-30 12:04:41 upgrade nginx:amd64 1.26.0-1 1.26.2-1
2025-12-30 12:04:41 status half-configured nginx:amd64 1.26.2-1
2025-12-30 12:04:41 configure nginx:amd64 1.26.2-1
2025-12-30 12:04:42 status installed nginx:amd64 1.26.2-1
Significado: Ocurrió una actualización de dpkg; si viste ETXTBSY alrededor de este momento, pudo ser durante scripts del mantenedor o un intento de postinst de reinicio.
Decisión: Si las actualizaciones de paquetes tocan componentes en uso, coordina reinicios y evita ejecutar despliegues de apps largos durante ejecuciones de apt.
Task 11: Comprobar reinicios pendientes (needrestart) e interpretar lo que te dice
cr0x@server:~$ sudo needrestart -r l
NEEDRESTART-VER: 3.6
Processes using old versions of upgraded files:
14231 /opt/myapp/current/myapp
Service restarts suggested:
systemctl restart myapp.service
Significado: Un proceso sigue usando una versión antigua mapeada de los archivos actualizados. Esta es la variante “lo reemplazaste, pero aún sigue ejecutándose”.
Decisión: Planifica un reinicio controlado. Si te importa el zero-downtime, haz rolling restarts instancia por instancia detrás de un load balancer.
Task 12: Validar el comportamiento de swap atómico de symlink (ln -sfn + readlink)
cr0x@server:~$ ls -l /opt/myapp
lrwxrwxrwx 1 root root 38 Dec 30 12:15 current -> /opt/myapp/releases/2025-12-30_121500
drwxr-xr-x 4 root root 4096 Dec 30 12:15 releases
cr0x@server:~$ sudo ln -sfn /opt/myapp/releases/2025-12-30_123000 /opt/myapp/current
cr0x@server:~$ readlink -f /opt/myapp/current
/opt/myapp/releases/2025-12-30_123000
Significado: El puntero current se movió al instante. Los procesos existentes siguen ejecutando el inode viejo; los nuevos arranques usan el nuevo target.
Decisión: Este es el primitivo de despliegue que quieres. Combínalo con un reinicio o reload gracioso.
Task 13: Probar la versión del binario en ejecución vs la versión en disco (checksum vía /proc)
cr0x@server:~$ sha256sum /opt/myapp/current/myapp
b1c5e3e2f4cbd9b5e6f3d5b2b5f5a9d6b9c8a7d5a3b2c1d0e9f8a7b6c5d4e3f2 /opt/myapp/current/myapp
cr0x@server:~$ sudo sha256sum /proc/14231/exe
9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1e0f9a8b /proc/14231/exe
Significado: El ejecutable en ejecución difiere del que actualmente apunta en disco. Tu rollout no ha tenido efecto para ese PID.
Decisión: Reinicia o rota el pool de procesos. No sigas redeployando; no cambiará los mapeos de memoria que ya existen.
Task 14: Si sospechas una carrera, atrapa quién lanza el binario durante el despliegue (auditoría con ps y timestamps)
cr0x@server:~$ ps -eo pid,lstart,cmd | grep -E '/opt/myapp/current/myapp' | grep -v grep
14231 Tue Dec 30 12:18:02 2025 /opt/myapp/current/myapp --config /etc/myapp/config.yaml
Significado: La hora de inicio del proceso coincide con el tiempo del despliegue. Esto suele significar que tu script de despliegue o supervisor lo reinició a mitad de copia.
Decisión: Haz que los pasos de despliegue sean idempotentes y serializados: stage release, cambia el puntero y luego reinicia (o recarga) una vez, no repetidamente.
Soluciones seguras que no convierten los despliegues en ruleta
Arreglar ETXTBSY en producción tiene menos que ver con “matar el proceso que sostiene el archivo” y más con adoptar un primitivo de despliegue que al kernel le guste. Debes dejar de tratar un ejecutable en ejecución como un blob mutable.
Arreglo 1: Usar directorios de release inmutables + swap atómico de puntero (symlink)
Este es el patrón que recomiendo con más frecuencia porque es aburrido, y aburrido es el mejor cumplido que puedes hacer a un sistema de despliegue.
Esquema:
/opt/myapp/releases/<release-id>/myapp(inmutable)/opt/myapp/current -> /opt/myapp/releases/<release-id>(symlink puntero)- El servicio systemd ejecuta
/opt/myapp/current/myapp
Pasos de despliegue:
- Subir a un nuevo directorio de release (aún no usado).
- Hacer health-check del binario en el directorio de release directamente.
- Cambiar el symlink
currentde forma atómica. - Reiniciar o recargar el servicio (idealmente de forma graciosa).
Por qué funciona: el proceso en ejecución sigue usando su inode viejo. La nueva release es un inode distinto en un directorio distinto. No hay escrituras en el lugar. No ETXTBSY. El rollback también es un cambio de symlink.
Arreglo 2: Escribir a un archivo temporal, fsync y luego renombrar (reemplazo atómico)
Si absolutamente debes mantener la misma ruta (p. ej., una herramienta de terceros lo espera), haz un reemplazo seguro:
- Descarga/escribe en
myapp.newen el mismo directorio. chmody verifica checksums.mv -f myapp.new myapp(rename es atómico en el mismo filesystem).
Importante: rename es atómico, pero no es magia. Si reemplazas un ejecutable en ejecución, rename suele estar permitido y es seguro, porque estás intercambiando entradas de directorio, no mutando el inode. El proceso en ejecución sigue usando el inode viejo.
Pero no combines esto con herramientas que hacen actualizaciones en el lugar (cp hacia la ruta final, rsync --inplace).
Arreglo 3: Deja de desplegar en directorios compartidos en vivo
Un anti-patrón común es desplegar en /usr/local/bin o un directorio de aplicación compartido donde múltiples servicios consumen helpers, plugins o runtimes. “Solo actualizas el helper” y de repente el despliegue de la API falla con ETXTBSY porque el helper se ejecuta durante el despliegue.
Mantén los artefactos de la aplicación privados por servicio, versionados, y reemplázalos mediante swap de puntero. Los directorios compartidos deben reservarse para paquetes gestionados por el sistema, actualizados bajo ventanas de mantenimiento controladas.
Arreglo 4: Patrones de systemd que juegan bien con rollouts
systemd no es la causa, pero puede amplificar carreras si tu despliegue dispara reinicios en momentos incómodos.
- Usa
ExecStartapuntando a la ruta del symlink estable. - Usa
ExecReloadpara recargas graciosas si tu app lo soporta. - Considera
Restart=on-failure(noalways) para reducir flapping durante errores de despliegue. - Si tienes múltiples workers, considera activación por socket o un proxy frontal para desacoplar el reinicio del impacto al cliente.
Arreglo 5: Mundo de contenedores: reconstruir imágenes, no parchear ejecutables en el lugar
Si ejecutas contenedores y “despliegas” copiando un nuevo binario dentro de un contenedor vivo, estás reinventando manualmente lo peor de la deriva de configuración.
Preferido:
- Construye una nueva imagen con el binario actualizado.
- Despliega la nueva imagen (actualización rolling).
- Usa tags inmutables o digests en producción, no “latest”.
Si debes usar un volumen montado para hot-swapping de artefactos, usa el patrón de release-dir + swap de symlink dentro del volumen. No hagas sobrescrituras en el lugar desde el host o sidecar.
Arreglo 6: Consideraciones de almacenamiento (porque “busy” puede esconder una historia de almacenamiento)
Como ingeniero de almacenamiento, diré la parte que muchos callan: ETXTBSY a menudo te muestra que tu despliegue espera semánticas de sistema de archivos local, pero le diste otra cosa.
- En NFS, asegura que tu directorio de artefactos de despliegue esté en un disco local si es posible.
- Si debes usar NFS: evita las actualizaciones en el lugar, prefiere directorios versionados y asegura opciones de montaje consistentes en la flota.
- En overlayfs: trata el filesystem del contenedor como inmutable; usa volúmenes para datos mutables.
Tres microhistorias del mundo corporativo (cómo afecta a los equipos)
Microhistoria #1: El incidente causado por una suposición equivocada
Tenían una pequeña flota de servidores Debian detrás de un load balancer. El proceso de despliegue era “simple”: copiar el binario nuevo en /opt/app/app, luego enviar una señal para reload. Funcionó durante años, que es como las malas suposiciones terminan convertidas en “decisiones de diseño”.
En un trimestre introdujeron un runner de jobs en background en los mismos hosts. Usaba el mismo binario, invocado con una flag distinta. El runner estaba supervisado por systemd y se reiniciaba agresivamente en fallos. Durante el despliegue, la canalización copió el binario mientras el servicio web estaba en ejecución. Eso a veces fallaba, pero no siempre. Luego empeoró: el runner se reinició a mitad del despliegue e intentó ejecutar el binario justo cuando el despliegue lo estaba truncando.
Resultado: ETXTBSY en los logs de despliegue, además de crashes ocasionales cuando un proceso ejecutó un binario parcialmente actualizado (porque algunos pasos corrían como distintos usuarios y no todos los hosts eran consistentes). Culparon a Debian. Debian era inocente; estaba haciendo su trabajo.
La solución no fue “reintentar hasta que funcione”. Pasaron a directorios de release inmutables. El servicio web y el runner ejecutaban ambos vía /opt/app/current/app symlink. El despliegue creó un release nuevo, cambió el symlink y reinició servicios en un orden controlado. La suposición equivocada fue “sobrescribir el archivo es equivalente a reemplazar el programa en ejecución”. No lo es.
Microhistoria #2: La optimización que salió mal
Un equipo de plataforma quería despliegues más rápidos y menos uso de disco. Alguien notó que copiar directorios completos de release consumía espacio y tiempo. Reemplazaron el enfoque de release-dir por un “rsync optimizado” a un directorio compartido único, usando --inplace para evitar archivos temporales y reducir la amplificación de escritura.
En VM de prueba tranquila rindió muy bien. En producción, los despliegues empezaron a fallar con ETXTBSY. Peor aún, surgió comportamiento parecido a corrupción bajo alta carga: algunos hosts quedaron con mezcla de assets viejos y nuevos porque rsync actualizó archivos en un orden que no coincidía con las expectativas de runtime de la app.
El equipo respondió con reintentos y timeouts más largos. Los tiempos de despliegue aumentaron. Las fallas se volvieron más raras pero más misteriosas. El load balancer estaba sano, pero usuarios vieron errores intermitentes porque distintos nodos servían versiones diferentes durante la ventana de sync parcial.
Revirtieron la “optimización” y volvieron a releases versionados. El uso de disco subió, predeciblemente. La confiabilidad también subió, que fue el único número que importó en la revisión del incidente. Lección: si tu “optimización” elimina atomicidad, no es optimización. Es deuda con buena imagen.
Microhistoria #3: La práctica aburrida pero correcta que salvó el día
Otra organización ejecutaba Debian 13 con gestión de cambios estricta. Su pipeline de despliegue siempre hacía stage de artefactos en un nuevo directorio nombrado con un release ID. Luego corría un chequeo canario que ejecutaba el binario desde la ruta stage, no desde el symlink en vivo. Solo después de pasar, cambiaba el symlink.
Un día, una actualización rutinaria del SO trajo una nueva librería runtime. Algunos servicios necesitaron reinicio para tomar los nuevos mapeos de librería. El equipo no lo notó inmediatamente, porque todo siguió corriendo. Pero después, durante un despliegue, sus chequeos compararon la suma de verificación del proceso en ejecución (vía /proc/<pid>/exe) con el artefacto stage. No coincidía, lo cual era esperado. Lo importante: el despliegue siguió teniendo éxito porque nadie intentó sobrescribir el binario en vivo.
Durante la ventana de mantenimiento reiniciaron servicios en rolling. Sin ETXTBSY, sin upgrades rotos, sin “por qué explotó el gestor de paquetes”. La práctica que los salvó fue terriblemente poco sexy: nunca mutar ejecutables en vivo; siempre intercambiar punteros; siempre poder revertir cambiando una sola cosa.
No “arreglaron” ETXTBSY porque rara vez lo disparaban. Ese es el estado ideal: prevenir la clase de fallo en lugar de volverse bueno peleando con él.
Errores comunes: síntoma → causa raíz → arreglo
1) Síntoma: cp falla con “Text file busy” al copiar un binario
Causa raíz: Estás sobrescribiendo el inode de un ejecutable en ejecución (la copia abre el destino con truncado/escritura).
Arreglo: Copia a un nombre nuevo y renombra, o despliega en un directorio de release nuevo y cambia un symlink.
2) Síntoma: error de rsync código 26 o 3 con “Text file busy”
Causa raíz: rsync está configurado para actualizar en el lugar (--inplace) o está apuntando a rutas en vivo que incluyen ejecutables.
Arreglo: Elimina --inplace. Usa --delay-updates para stage de actualizaciones y luego mover archivos temporales de forma atómica, o mejor: directorios de release.
3) Síntoma: el despliegue “tiene éxito”, pero el servicio en ejecución sigue siendo la versión antigua
Causa raíz: Reemplazaste el archivo en disco, pero el proceso sigue usando el inode viejo (posiblemente ya borrado). Comportamiento clásico de Unix.
Arreglo: Reinicia el servicio (rolling restart). Valida vía checksum de /proc/<pid>/exe o needrestart.
4) Síntoma: el error ocurre solo en contenedores, no en metal
Causa raíz: Estás parchando el filesystem vivo del contenedor o un volumen bind-mounted mientras el binario se ejecuta dentro del contenedor.
Arreglo: Construye y despliega una nueva imagen. Si usas volúmenes para artefactos, usa releases inmutables y swap de puntero.
5) Síntoma: ETXTBSY aparece durante actualizaciones de paquetes o unattended-upgrades
Causa raíz: Un servicio está ejecutándose mientras los paquetes intentan actualizar ejecutables o helpers relacionados; scripts del mantenedor pueden ejecutar herramientas en medio de la actualización.
Arreglo: Coordina upgrades con reinicios de servicio, evita superponer despliegues de apps con apt runs y usa needrestart para gestionar reinicios.
6) Síntoma: solo algunos hosts fallan, usualmente los bajo carga
Causa raíz: La ventana de carrera depende de la carga: IO más lenta significa que tu ventana de sobrescritura se solapa más con eventos de exec/restart. O los scripts de despliegue se comportan distinto por tiempos.
Arreglo: Elimina la carrera: no escrituras en el lugar; solo cambios atómicos de puntero; serializa acciones de despliegue; reduce “storms” de reinicio.
7) Síntoma: “mv: Text file busy” aunque rename debería ser atómico
Causa raíz: A menudo indica que no estás haciendo un rename dentro del mismo filesystem, o estás en un filesystem con semánticas especiales (overlayfs/NFS).
Arreglo: Asegura que el archivo temporal esté en el mismo directorio (mismo mount). Revisa stat -f o mount y ajusta la ubicación de despliegue.
Listas de verificación / plan paso a paso
Contención inmediata (durante un incidente)
- Congela más intentos de despliegue a los mismos hosts (detén el flapping).
- Identifica la ruta que disparó ETXTBSY y el comando que lo provocó.
- Usa
lsofofuserpara identificar PIDs que usan el archivo. - Decide si puedes reiniciar con seguridad:
- Si sí: haz un reinicio controlado (un host a la vez si es necesario).
- Si no: despliega en un nuevo directorio de release y planifica un corte por fases gracioso.
- Verifica la versión en ejecución vía checksum de
/proc/<pid>/exeo salida de versión.
Remediación permanente (hacer que deje de ocurrir)
- Adopta una de estas políticas:
- directorios de release + swap de symlink (recomendado)
- archivo temporal + fsync + rename atómico (aceptable)
- despliegues basados en imágenes para contenedores (mejor para workloads conteinerizados)
- Audita las herramientas de despliegue por escrituras en el lugar:
- elimina
--inplacede rsync - evita
cpdirecto a la ruta final del ejecutable - evita “descargar directamente en el archivo final”
- elimina
- Haz los reinicios explícitos:
- reinicio systemd en la pipeline después del swap de puntero
- rolling restarts detrás del LB
- recarga graciosa donde se soporte
- Agrega una barrera de despliegue:
- rechaza desplegar si
lsofmuestra que vas a sobrescribir un ejecutable mapeado - rechaza si la ruta objetivo está en NFS/overlay a menos que uses directorios de release
- rechaza desplegar si
- Operationaliza el rollback:
- mantén N releases anteriores
- symlink de vuelta + reinicio
Lista de verificación de verificación (probar que la corrección funciona)
- Despliega una nueva release con el servicio en ejecución. Confirma que no hay ETXTBSY.
- Confirma que el symlink cambió y apunta al nuevo directorio.
- Reinicia el servicio y confirma que el checksum en ejecución coincide con el artefacto deseado.
- Revierte intercambiando el symlink a la release previa; reinicia; confirma el rollback del checksum.
- Ejecuta dos despliegues seguidos y asegura que no exista estado parcial entre ellos.
Datos interesantes y contexto histórico
- El nombre de error ETXTBSY precede a Linux: viene de Unix temprano, donde “text segment” era el término formal para el código ejecutable.
- Unix te permite borrar ejecutables en ejecución: un proceso en ejecución mantiene una referencia abierta, así que la entrada de directorio puede desaparecer mientras la ejecución continúa.
- Rename es atómico (localmente): en sistemas de archivos POSIX locales, intercambiar entradas de directorio con
rename()es atómico dentro del mismo filesystem, por eso los swaps de puntero son tan efectivos. - Las actualizaciones en el lugar fueron históricamente tentadoras: los administradores lo hacían para ahorrar espacio y evitar “binarios duplicados”, especialmente cuando los discos eran pequeños y caros.
- Los sistemas de archivos en red complican las invariantes: la coherencia de caché de NFS y el comportamiento del cliente hicieron históricamente que “atómico e inmediato” fuera una promesa condicional.
- Los contenedores no cambiaron el kernel: los namespaces aíslan procesos, pero las semánticas de ejecución de archivos siguen las mismas reglas del kernel; ETXTBSY no es “un bug de contenedores”.
- Los gestores de paquetes aprendieron lecciones duras: dpkg y similares dependen mucho de reemplazos basados en rename y staging cuidadoso porque sobrescribir binarios del sistema en vivo es una forma segura de romper upgrades.
- El código que ya está en ejecución no se actualiza a sí mismo: reemplazar el archivo en disco no parchea las páginas ya mapeadas en memoria; aún necesitas reinicio/recarga.
Preguntas frecuentes
1) ¿Es “Text file busy” un bug de Debian 13?
No. Es comportamiento del kernel mostrado por tu método de despliegue. Debian 13 solo sucede ser el lugar donde tu timing, carga o capa de almacenamiento lo hicieron visible.
2) ¿Por qué rm a veces funciona pero cp falla?
rm unlinkea la entrada de directorio; el proceso en ejecución mantiene el inode abierto. cp sobrescribe/trunca el mismo inode, lo cual el kernel bloquea cuando se está ejecutando.
3) ¿Puedo arreglarlo añadiendo reintentos al despliegue?
Puedes reducir el ruido en el pager, claro. No arreglarás la carrera subyacente, y eventualmente caerás en un host donde el timing nunca coincida. Reemplaza el primitivo de despliegue en su lugar.
4) Si rename es seguro, ¿por qué vi mv: Text file busy?
Normalmente porque no era un rename real dentro del mismo filesystem (movimiento entre dispositivos), o estás en una capa de filesystem con semánticas especiales (overlayfs, NFS). Asegura que el archivo temporal se cree en el mismo directorio y mount.
5) ¿Esto afecta a scripts también, o solo a binarios?
Mayormente a binarios, pero los scripts pueden provocar problemas similares cuando se ejecutan a través de un intérprete que los abre de una forma que activa comprobaciones de ocupado, o cuando tu despliegue ejecuta el script mientras lo reescribe. No mutar ningún punto de entrada “vivo” en el lugar.
6) ¿Cómo pruebo qué proceso está bloqueando el despliegue?
Usa lsof <path> y busca mapeos txt o descriptores abiertos. fuser -v también es útil para una lista rápida de PIDs.
7) ¿Parar el servicio siempre lo arregla?
Típicamente eliminará ETXTBSY para ese archivo, sí. Pero parar servicios como mecanismo de despliegue implica downtime. Prefiere swaps atómicos más reinicios controlados para predictibilidad.
8) ¿Cuál es el patrón de despliegue más seguro “sin sorpresas” en Debian?
Directorios de release inmutables + swap atómico de symlink + reinicio controlado por systemd (rolling across nodes). Evita ETXTBSY y previene despliegues parciales.
9) ¿Necesito reiniciar después de actualizar un binario si la ruta se mantiene igual?
Sí, si quieres que el proceso en ejecución use el nuevo código. Un proceso en ejecución no remapea automáticamente sus páginas ejecutables solo porque el archivo en disco cambió.
10) ¿Qué hago si no puedo reiniciar (tiempos reales estrictos o sesiones muy largas)?
Entonces la solución es arquitectónica: ejecuta múltiples instancias, drena conexiones o usa un supervisor/proxy que soporte handoff gracioso. ETXTBSY es un síntoma; “no se permiten reinicios” es la restricción real.
Conclusión: próximos pasos que puedes hacer hoy
ETXTBSY no es Linux siendo difícil. Es Linux aplicando un límite que tu proceso de despliegue no debería cruzar. Sobrescribir un ejecutable en ejecución en el lugar es como cambiar una rueda mientras el coche va a velocidad de autopista: técnicamente puedes intentarlo, pero no te gustará el resultado.
Pasos prácticos:
- Encuentra la ruta exacta que dispara “Text file busy” e identifica el PID que la mantiene con
lsof. - Audita tu pipeline por escrituras en el lugar (
cpa la ruta final,rsync --inplace, descargas directas en ubicaciones en vivo). - Escoge un primitivo de despliegue seguro y estandarízalo:
- directorios de release + swap de symlink (mejor general)
- temporal + fsync + rename (si debes mantener la ruta)
- nuevas imágenes de contenedor (si estás conteinerizado)
- Haz que los reinicios sean deliberados y rolling, no accidentales y compitiendo con tu paso de copia.
- Agrega una barrera: rehúsa desplegar si el ejecutable objetivo está mapeado (
lsofmuestratxt) y vas a sobrescribirlo.
Haz esas cinco cosas, y “Text file busy” volverá a ser un error que lees en los postmortems de otros. Donde pertenece.